androidreact-nativekotlingradleexpo

How to conditionally exclude Expo package(s) from Android build in Expo SDK 53?


Context

Expo documentation states it's possible to exclude Expo packages from the Android build by calling the useExpoModules function in the project's settings.gradle file.

useExpoModules seems to be defined in expo-modules-autolinking/scripts/android/autolinking_implementation.gradle. It accepts a map of options, one of which is exclude, which is a list of Expo packages to exclude. settings.gradle has a reference to this function via the apply from syntax.

Here's an example from the Expo Go app's settings.gradle file:

// ...

apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")

// ...

useExpoModules([
    searchPaths: [
        '../../../packages'
    ],
    exclude : [
        'expo-module-template',
        'expo-module-template-local',
        'react-native-reanimated',
        'expo-dev-menu-interface',
        'expo-dev-menu',
        'expo-dev-launcher',
        'expo-dev-client',
        'expo-maps',
        'expo-network-addons',
        'expo-splash-screen',
        '@expo/ui',
        'expo-mesh-gradient'
    ]
])

In my Expo project, using SDK version 52, I conditionally exclude certain packages from the Android build. For example, my settings.gradle would have something like this:

if (/* condition */) {
  useExpoModules([
    exclude: [/* List of excluded packages */]
  ])
} else {
  useExpoModules()
}

Problem

The above works in SDK 52, however SDK 53 (which I am upgrading to) changes how useExpoModules is defined and called.

The function is defined in the expo-autolinking-settings-plugin and does not seem to accept any options via parameters but references options via properties on the class it's defined on:

// ...

open class ExpoAutolinkingSettingsExtension(
  val settings: Settings,
  @Inject val objects: ObjectFactory
) {

// ...

  var exclude: List<String>? = null

// ...

  fun useExpoModules() {
    SettingsManager(
      settings,
      searchPaths,
      ignorePaths,
      exclude
    ).useExpoModules()
  }

// ...

}

See the "bare minimum" Expo template for an example of how the plugin is applied and the function called:

// ...

plugins {
  // ...

  id("expo-autolinking-settings")
}

// ...

expoAutolinking.useExpoModules()

// ...

With this information, how might I (conditionally) exclude Expo package(s) from the Android build in Expo SDK 53?

What I've tried

Using the old arguments

expoAutolinking.useExpoModules([ exclude: [/* List of excluded packages */] ]), predictably, does not work, raising the following error:

Could not find method useExpoModules() for arguments [{exclude=[ ... ]}] on extension 'expoAutolinking' of type expo.modules.plugin.ExpoAutolinkingSettingsExtension.

Setting expoAutolinking.exclude

For example:

// ...

if (/* condition */) {
  expoAutolinking.exclude = [/* List of excluded packages */]
}

expoAutolinking.useExpoModules()

// ...

This also does not seem to work because the packages which are supposed to be excluded are still included (according to build logs (logged from here) and an error originating from a task from a package which was supposed to be excluded).

I then added the following logs: (listed in call order):

All logs correctly logged a list of excluded packages. This means Expo is correctly receiving the list of excluded packages, right?


Solution

  • Adding a log within the logic that resolves the auto-linking configuration in SettingsManager.kt (source) revealed the issue. The logged command was:

    [
      node,
      --no-warnings,
      --eval,
      require(require.resolve('expo-modules-autolinking', { paths: [require.resolve('expo/package.json')] }))(process.argv.slice(1)),
      --,
      resolve,
      --platform,
      android,
      --json,
      --exclude,
      excluded-package-1 excluded-package-2 excluded-package-n
    ]
    

    The excluded packages were passed as a single argument to the command that resolves the details for each package. As a result, the script was excluding a package by the name of all the excluded packages joined together which doesn't exist so can't be excluded.

    The reason why all the excluded packages gets passed as a single argument is because they are not split up:

      fun build(): List<String> {
        val command = baseCommand +
          autolinkingCommand +
          platform +
          useJson +
          optionsMap.map { (key, value) -> listOf("--$key", value) }.flatMap { it } +
          searchPaths
        return Os.windowsAwareCommandLine(command)
      }
    

    – source

    optionsMap.map { (key, value) -> listOf("--$key", value) }.flatMap { it }
    

    should be something like

    optionsMap.flatMap { (key, value) ->
      value.split(" ").filter { it.isNotEmpty() }.map { subvalue -> listOf("--$key", subvalue) }
    }.flatMap { it }