import groovy.json.JsonSlurper import java.nio.file.Paths // Object representing a module. class ExpoModule { // Name of the JavaScript package String name // Version of the package, loaded from `package.json` String version // Name of the Android project String projectName // Path to the folder with Android project String sourceDir ExpoModule(Object data) { this.name = data.packageName this.version = data.packageVersion this.projectName = data.projectName this.sourceDir = data.sourceDir } } class ExpoAutolinkingManager { private File projectDir private Map options private Object cachedResolvingResults static String generatedPackageListNamespace = 'expo.modules.linker' static String generatedPackageListFilename = 'ExpoModulesPackageList.java' static String generatedFilesSrcDir = 'generated/expo/src/main/java' static String modulesLinkerName = 'expo-modules-linker' 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) 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, the generated list should be empty. args += '--empty' } exec(args) } static String exec(String[] commandArgs) { Process proc = commandArgs.execute() 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' ] if (options?.searchPaths) { args += options.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" } // 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) { include(":${module.projectName}") project(":${module.projectName}").projectDir = new File(module.sourceDir) if (module.projectName == "expo-modules-core") { String modulesLinkerPath = Paths.get( module.sourceDir, "..", "android-linker" ).toString() include(":${ExpoAutolinkingManager.modulesLinkerName}") project(":${ExpoAutolinkingManager.modulesLinkerName}").projectDir = new File(modulesLinkerPath) } } // Save the manager in the shared context, so that we can later use it in `build.gradle`. gradle.ext.expoAutolinkingManager = manager } } else { def addDependencies = { DependencyHandler handler, Closure projectNameResolver -> // Return early if `useExpoModules` was not called in `settings.gradle` if (!gradle.ext.has('expoAutolinkingManager')) { // 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`: expo modules won\'t be autolinked.') return } ExpoAutolinkingManager manager = gradle.ext.expoAutolinkingManager def modules = manager.getModules() if (!modules.length) { return } println 'Using expo modules' for (module in modules) { Project dependency = projectNameResolver(module) // Don't link itself if (module.name == "expo-modules-core") { continue } if (manager.shouldUseAAR()) { handler.add('api', "${dependency.group}:${module.projectName}:${dependency.version}") } else { handler.add('api', dependency) } // Can remove this once we move all the interfaces into the core. if (dependency.name.endsWith('-interface')) { continue } println "— ${Colors.GREEN}${module.name}${Colors.RESET} (${module.version})" } } def findAppProject = { Project project -> return project.rootProject.subprojects.find { it.plugins.find { it.class.name == "com.android.build.gradle.AppPlugin" } } } ext.addLinkerPackage = { Project project -> Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null if (options == null) { return } if (options.autolink == false) { return } Project appProject = findAppProject(project) if (appProject == null) { logger.warn("Can't add `expo-modules-linker` automatically. The app android project wasn't found.") return } Project linkerProject = rootProject.findProject(":${ExpoAutolinkingManager.modulesLinkerName}") if (linkerProject == null) { logger.warn("Can't find `expo-modules-linker` project. Please, make sure that `useExpoModules` was called in `settings.gradle`.") return } if (appProject.configurations.findByName("implementation")) { appProject.dependencies.add('implementation', linkerProject) } else { logger.warn("Can't add `expo-modules-linker` automatically. Couldn't find configuration.") } } // Adding dependencies ext.addExpoModulesDependencies = { DependencyHandler handler, Project project -> // `addExpoModulesDependencies` can't be called in `expo-modules-core`, because it would lead to the circular dependency. // For some reason Gradle sometimes tries to call this function in `expo-modules-core` - maybe because `expo-modules-core` adds `expo-modules-linker` // and Gradle tries to parse linker project with the wrong context. So for now, we're just ignoring that and waiting for the correct invocation. if (project.path == ":expo-modules-core" || project.path.startsWith("host.exp.exponent:expo-modules-core")) { logger.warn("`addExpoModulesDependencies` was called with the `expo-modules-core` project.") return } addDependencies(handler) { module -> rootProject.project(":${module.projectName}") } } // 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) { def dependency = project.findProject(":${module.projectName}") if (dependency == null) { logger.warn("Coudn't find project ${module.projectName}. Please, make sure that `useExpoModules` was called in `settings.gradle`.") continue } if (module.projectName == "expo-modules-core") { continue } project.evaluationDependsOn(":${module.projectName}") } } }