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.

EDIT: I’ve found an easier way to set *SdkVersion without writing your own plugin

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?

1 Like

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.

@wodin The package you guys used is deprecated and superseded by FFmpegKit now. GitHub - tanersener/react-native-ffmpeg: FFmpeg for react-native. Not maintained anymore. Superseded by FFmpegKit.

And here is the new Package living.

Did anyone create an expo plugin for this one by any chance?

1 Like

I have not looked at this again since September, but the installation instructions look very promising. As far as I can see, for Android, all you need to do is add the following to build.gradle:

ext {
    ffmpegKitPackage = "<package name>"
}

It’s not clear to me whether you’d need to specify the minSdkVersion or make it pick the first libfbjni.so or libc++_shared.so, but I suspect you might.

Anyway it seems like you should be able to make some minor modifications to my config plugin to get it to work with ffmpeg-kit.

1 Like

I still have a couple of weeks until I need to have a look at this :smiley:

Hey!
thanks for your example, it helped me a lot :slight_smile:

How do you test your plugin easily ?
Currently I deploy it to github, the yarn add <MY_PLUGIN>#MY_BRANCH, but it is a bit annoying…

Hi

I did not bother with making an npm out of the plugins. I just created a “plugin” subdirectory in the app, saved the plugins there and then referenced them using “./plugin/xxx.js”.

Then I tested by running expo prebuild.

1 Like

I’ve been working on integrating ffmpeg-kit-react-native for a bit now. Specifically on android I’m running into the same issue @wodin mentioned with duplicate libc++_shared.so. I found that this is a common issue with the library which has a known fix. However I’m not 100% how to implement this change with a config plugin because I don’t know really know the difference between withAppBuildGradle and withProjectBuildGradle mods from expo/config-plugins. Currently I’m working based off of @bacon’s plugin on the @config-plugins/ffmpeg-kit-react-native repo. I’ll let you know if I get it working with a patch-package.

Use this config-plugins/README.md at master · expo/config-plugins · GitHub

2 Likes

@charles.goode see if my withPickFirstFbjni stuff above does what you need.

Also this:

The great @bacon released an expo-config plugin for ffmpeg-kit.

[EDIT]
Whoops, looks like he already posted this. Sorry!

1 Like

Yes, he said so up thread :point_up:

:slight_smile:

@charles.goode seemed still to be having a problem that I thought the pickFirst part of my Config Plugin or else the react-native-pdf one would help him with.