import groovy.json.JsonSlurper import java.nio.file.Paths // Object representing a gradle project. class ExpoModuleGradleProject { // Name of the Android project String name // Path to the folder with Android project String sourceDir ExpoModuleGradleProject(Object data) { this.name = data.name this.sourceDir = data.sourceDir } } // Object representing a module. class ExpoModule { // Name of the JavaScript package String name // Version of the package, loaded from `package.json` String version // Gradle projects ExpoModuleGradleProject[] projects ExpoModule(Object data) { this.name = data.packageName this.version = data.packageVersion this.projects = data.projects.collect { new ExpoModuleGradleProject(it) } } } class ExpoAutolinkingManager { private File projectDir private Map options private Object cachedResolvingResults static String generatedPackageListNamespace = 'expo.modules' static String generatedPackageListFilename = 'ExpoModulesPackageList.java' static String generatedFilesSrcDir = 'generated/expo/src/main/java' ExpoAutolinkingManager(File projectDir, Map options = [:]) { this.projectDir = projectDir this.options = options } Object resolve() { if (cachedResolvingResults) { return cachedResolvingResults } String[] args = convertOptionsToCommandArgs('resolve', this.options) args += ['--json'] String output = exec(args, projectDir) Object json = new JsonSlurper().parseText(output) cachedResolvingResults = json return json } boolean shouldUseAAR() { return options?.useAAR == true } ExpoModule[] getModules() { Object json = resolve() return json.modules.collect { new ExpoModule(it) } } static void generatePackageList(Project project, Map options) { String[] args = convertOptionsToCommandArgs('generate-package-list', options) // Construct absolute path to generated package list. def generatedFilePath = Paths.get( project.buildDir.toString(), generatedFilesSrcDir, generatedPackageListNamespace.replace('.', '/'), generatedPackageListFilename ) args += [ '--namespace', generatedPackageListNamespace, '--target', generatedFilePath.toString() ] if (options == null) { // Options are provided only when settings.gradle was configured. // If not or opted-out from autolinking, the generated list should be empty. args += '--empty' } exec(args, project.rootDir) } static String exec(String[] commandArgs, File dir) { Process proc = commandArgs.execute(null, dir) StringBuffer outputStream = new StringBuffer() proc.waitForProcessOutput(outputStream, System.err) return outputStream.toString() } static private String[] convertOptionsToCommandArgs(String command, Map options) { String[] args = [ 'node', '--eval', 'require(\'expo-modules-autolinking\')(process.argv.slice(1))', '--', command, '--platform', 'android' ] def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null)) if (searchPaths) { args += searchPaths } if (options?.ignorePaths) { args += '--ignore-paths' args += options.ignorePaths } if (options?.exclude) { args += '--exclude' args += options.exclude } return args } } class Colors { static final String GREEN = "\u001B[32m" static final String RESET = "\u001B[0m" } // We can't cast a manager that is created in `settings.gradle` to the `ExpoAutolinkingManager` // because if someone is using `buildSrc`, the `ExpoAutolinkingManager` class // will be loaded by two different class loader - `settings.gradle` will use a diffrent loader. // In the JVM, classes are equal only if were loaded by the same loader. // 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. def validateExpoAutolinkingManager(manager) { assert ExpoAutolinkingManager.name == manager.getClass().name return manager } // Here we split the implementation, depending on Gradle context. // `rootProject` is a `ProjectDescriptor` if this file is imported in `settings.gradle` context, // otherwise we can assume it is imported in `build.gradle`. if (rootProject instanceof ProjectDescriptor) { // Method to be used in `settings.gradle`. Options passed here will have an effect in `build.gradle` context as well, // i.e. adding the dependencies and generating the package list. ext.useExpoModules = { Map options = [:] -> ExpoAutolinkingManager manager = new ExpoAutolinkingManager(rootProject.projectDir, options) ExpoModule[] modules = manager.getModules() for (module in modules) { for (moduleProject in module.projects) { include(":${moduleProject.name}") project(":${moduleProject.name}").projectDir = new File(moduleProject.sourceDir) } } // Save the manager in the shared context, so that we can later use it in `build.gradle`. gradle.ext.expoAutolinkingManager = manager } } else { def addModule = { DependencyHandler handler, String projectName, Boolean useAAR -> Project dependency = rootProject.project(":${projectName}") if (useAAR) { handler.add('api', "${dependency.group}:${projectName}:${dependency.version}") } else { handler.add('api', dependency) } } def addDependencies = { DependencyHandler handler, Project project -> def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager) def modules = manager.getModules() if (!modules.length) { return } println '' println 'Using expo modules' for (module in modules) { // Don't link itself if (module.name == project.name) { continue } // Can remove this once we move all the interfaces into the core. if (module.name.endsWith('-interface')) { continue } for (moduleProject in module.projects) { addModule(handler, moduleProject.name, manager.shouldUseAAR()) println " - ${Colors.GREEN}${moduleProject.name}${Colors.RESET} (${module.version})" } } println '' } // Adding dependencies ext.addExpoModulesDependencies = { DependencyHandler handler, Project project -> // Return early if `useExpoModules` was not called in `settings.gradle` if (!gradle.ext.has('expoAutolinkingManager')) { logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.') return } def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager) if (rootProject.findProject(':expo-modules-core')) { // `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off. addModule(handler, 'expo-modules-core', manager.shouldUseAAR()) } else { logger.error('Error: `expo-modules-core` project is not included by autolinking.') } // If opted-in not to autolink modules as dependencies if (manager.options == null) { return } addDependencies(handler, project) } // Generating the package list ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir ext.generateExpoModulesPackageList = { // Get options used in `settings.gradle` or null if it wasn't set up. Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null if (options == null) { // TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2 // logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.') } ExpoAutolinkingManager.generatePackageList(project, options) } ext.ensureDependeciesWereEvaluated = { Project project -> if (!gradle.ext.has('expoAutolinkingManager')) { return } def modules = gradle.ext.expoAutolinkingManager.getModules() for (module in modules) { for (moduleProject in module.projects) { def dependency = project.findProject(":${moduleProject.name}") if (dependency == null) { logger.warn("Coudn't find project ${moduleProject.name}. Please, make sure that `useExpoModules` was called in `settings.gradle`.") continue } // Prevent circular dependencies if (moduleProject.name == project.name) { continue } project.evaluationDependsOn(":${moduleProject.name}") } } } }