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

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