Managed EAS build - How to edit build.gradle via mods.

Expo CLI 4.8.1 environment info:
    System:
      OS: Linux 5.8 KDE neon 5.22
      Shell: 5.0.17 - /bin/bash
    npmPackages:
      expo: ^42.0.0 => 42.0.0 
      react: ^17.0.1 => 17.0.2 
      react-dom: ^17.0.1 => 17.0.2 
      react-native: https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz => 0.63.2 
      react-native-web: ~0.15.0 => 0.15.7 
    Expo Workflow: managed

eas-cli/0.22.0 linux-x64 node-v14.17.3

I’m trying to add react-native-ffmpeg to my project with video-lts packages (LTS use minSdkVersion 16).
How to edit build.gradle to define package name in ext?
Or how to increase minSdkVersion to required level?

import { ExpoConfig, ConfigContext } from '@expo/config';
import { ConfigPlugin, withAppBuildGradle } from '@expo/config-plugins';

const withFfmpegMod: ConfigPlugin = (config) => {
	return withAppBuildGradle(config, config => {
		config.mods = {
			...config.mods,
			android: {
				appBuildGradle: (config) => {
					config.modResults.contents = `ext {
    reactNativeFFmpegPackage = "video-lts"
}`;
					return config;
				},
			},
		};
		return config;
	});
};

export default ({ config }: ConfigContext): ExpoConfig => withFfmpegMod(config as ExpoConfig);

"Run gradlew" phase.
See http://g.co/androidstudio/manifest-merger for more information about the manifest merger.

[stderr] /build/workingdir/build/android/app/src/debug/AndroidManifest.xml Error:

[stderr] 	uses-sdk:minSdkVersion 21 cannot be smaller than version 24 declared in library [:react-native-ffmpeg] /build/workingdir/build/node_modules/react-native-ffmpeg/android/build/intermediates/library_manifest/debug/AndroidManifest.xml as the library might be using APIs not available in 21

[stderr] 	Suggestion: use a compatible library with a minSdk of at most 21,

[stderr] 		or increase this project's minSdk version to at least 24,

[stderr] 		or use tools:overrideLibrary="com.arthenica.reactnative" to force usage (may lead to runtime failures)

> Task :unimodules-core:compileDebugKotlin

[stderr] FAILURE: Build failed with an exception.

[stderr] * What went wrong:

[stderr] Execution failed for task ':app:processDebugMainManifest'.

[stderr] > Manifest merger failed : uses-sdk:minSdkVersion 21 cannot be smaller than version 24 declared in library [:react-native-ffmpeg] /build/workingdir/build/node_modules/react-native-ffmpeg/android/build/intermediates/library_manifest/debug/AndroidManifest.xml as the library might be using APIs not available in 21

[stderr]   	Suggestion: use a compatible library with a minSdk of at most 21,

[stderr]   		or increase this project's minSdk version to at least 24,

[stderr]   		or use tools:overrideLibrary="com.arthenica.reactnative" to force usage (may lead to runtime failures)

[stderr] * Try:

[stderr] Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

[stderr] * Get more help at https://help.gradle.org

[stderr] BUILD FAILED in 2m 1s

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.

Use '--warning-mode all' to show the individual deprecation warnings.

See https://docs.gradle.org/6.8/userguide/command_line_interface.html#sec:command_line_warnings

429 actionable tasks: 429 executed

Error: Gradle build failed with unknown error. Please see logs for the "Run gradlew" phase.

Hey @expo_karaushu

I just managed to get this to work! (Android only for now).

This is my first time trying to write my own config plugins, so I’m sure some things could be done better. Also, I don’t like all the manual config.replace(/regex/, string) and the brute force way I’m replacing the packagingOptions, but at least it seems to work!

I only tried building for Android so far and have not actually tested the results yet.

I created the following files:

plugins/withMinAndroidSdkVersion.js

const {
  withProjectBuildGradle,
  withPlugins,
} = require('@expo/config-plugins');

function setMinSdkVersion(buildGradle, minVersion) {
  const regexpMinSdkVersion = /\bminSdkVersion\s*=\s*(\d+)/;
  const match = buildGradle.match(regexpMinSdkVersion);

  if (match) {
    const version = parseInt(match[1], 10);

    if (version < minVersion) {
      buildGradle = buildGradle.replace(
        /\bminSdkVersion\s*=\s*\d+/,
        `minSdkVersion = ${minVersion}`
      );
    } else {
      console.warn(`WARN: minSdkVersion is already >= ${version}`);
    }
  }

  return buildGradle;
}

const withMinSdkVersion = (config, { minSdkVersion } = {}) => {
  return withProjectBuildGradle(config, (config) => {
    if (config.modResults.language === 'groovy') {
      config.modResults.contents = setMinSdkVersion(
        config.modResults.contents,
        minSdkVersion
      );
    } else {
      throw new Error(
        "Can't set minSdkVersion in the project build.gradle, because it's not groovy"
      );
    }
    return config;
  });
};

module.exports = (config, props) =>
  withPlugins(config, [
    [withMinSdkVersion, props],
  ]);

plugins/withFfmpegPackage.js

const {
  withAppBuildGradle,
  withProjectBuildGradle,
  withPlugins,
} = require('@expo/config-plugins');

function setFfmpegPackage(buildGradle, packageName) {
  const regexpReactNativeFfmpegPackage =
    /\breactNativeFFmpegPackage\s*=\s*"([^"]*)"/;
  const match = buildGradle.match(regexpReactNativeFfmpegPackage);

  if (match) {
    return buildGradle.replace(
      regexpReactNativeFfmpegPackage,
      `reactNativeFFmpegPackage = "${packageName}"`
    );
  }

  // Set the ffmpeg native package
  return buildGradle.replace(
    /\bext\s?{/,
    `ext {
        reactNativeFFmpegPackage = "${packageName}"`
  );
}

function addPickFirst(buildGradle, paths) {
  const regexpPackagingOptions = /\bpackagingOptions\s*{[^}]*}/;
  const packagingOptionsMatch = buildGradle.match(
    regexpPackagingOptions
  );

  let bodyLines = [];
  paths.forEach((path) => {
    bodyLines.push(`        pickFirst '${path}'`);
  });
  let body = bodyLines.join('\n');

  if (packagingOptionsMatch) {
    console.warn(
      'WARN: Replacing packagingOptions in app build.gradle'
    );
    return buildGradle.replace(
      regexpPackagingOptions,
      `packagingOptions {
${body}
    }`
    );
  }

  const regexpAndroid = /\nandroid\s*{/;
  const androidMatch = buildGradle.match(regexpAndroid);

  if (androidMatch) {
    return buildGradle.replace(
      regexpAndroid,
      `
android {
    packagingOptions {
${body}
    }`
    );
  }

  throw new Error('Could not find where to add packagingOptions');
}

const withFfmpegPackage = (
  config,
  { ffmpegPackage: packageName = 'min' } = {}
) => {
  return withProjectBuildGradle(config, (config) => {
    if (config.modResults.language === 'groovy') {
      config.modResults.contents = setFfmpegPackage(
        config.modResults.contents,
        packageName
      );
    } else {
      throw new Error(
        "Can't set ffmpeg package name in the project build.gradle, because it's not groovy"
      );
    }
    return config;
  });
};

const withPickFirstFbjni = (config, props = {}) => {
  return withAppBuildGradle(config, (config) => {
    if (config.modResults.language === 'groovy') {
      config.modResults.contents = addPickFirst(
        config.modResults.contents,
        ['lib/**/libfbjni.so', 'lib/**/libc++_shared.so']
      );
    } else {
      throw new Error(
        "Can't add pickFirst '**/libfbjni.so' because app build.grandle is not groovy"
      );
    }
    return config;
  });
};

module.exports = (config, props) =>
  withPlugins(config, [
    [withFfmpegPackage, props],
    [withPickFirstFbjni, props],
  ]);

Then I added the plugins to app.json like this:

    "plugins": [
      [
        "./plugins/withMinAndroidSdkVersion.js",
        {
          "minSdkVersion": 24
        }
      ],
      [
        "./plugins/withFfmpegPackage.js",
        {
          "ffmpegPackage": "full"
        }
      ]
    ]

This took a lot of trial and error to get it to build. I had first got the ffmpeg package name working, but then got the same error about the minSdkVersion as you got. I wrote another plugin to fix that and then I got an error about duplicate libfbjni.so files:

[stderr] FAILURE: Build failed with an exception.
[stderr] * What went wrong:
[stderr] Execution failed for task ':app:mergeDebugNativeLibs'.
[stderr] > A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
[stderr]    > More than one file was found with OS independent path 'lib/x86/libfbjni.so'. If you are using jniLibs and CMake IMPORTED targets, see https://developer.android.com/studio/preview/features#automatic_packaging_of_prebuilt_dependencies_used_by_cmake

I managed to fix that, after which I got the same error, but this time about libc++_shared.so.

When I first started this I got complaints about not being able to use import and export. How are you doing it that you did not get those errors? Or is that just a side effect of using TypeScript?

@charliecruzan, do you have any suggestions on how to improve my config plugins?

Hi, thanks for reply. For me easiest solution was expo prebuild. I change minSDKVersion and just followed instructions for android/build.gradle. And EAS build passed well.

1 Like

Sure, that’s basically the Bare workflow, which the Expo team has made much easier these days.