google/bundletool

Bundle error when using services to load dynamic feature modules

rfha opened this issue · 9 comments

rfha commented

Describe the bug
When using ServiceLoaders to load dynamic feature modules, bundle tool throws an error if there are implementations of the same service interface within different modules.

Bundletool version(s) affected
Version: 0.12.0

Stacktrace
Execution failed for task ':app:packageReleaseBundle'.
A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
Modules 'base' and 'other' contain entry 'root/META-INF/services/com.package.ServiceImplementation' with different content.

To Reproduce
Create a project with one base and one dynamic feature module.
Add a service interface and an implementation for this service in each module.
Annotate the services with the google AutoService or add the service definitions manually to META-INF/services.
Now try to build an app bundle.

Expected behavior
The expected behaviour would be that each split apk would have the service definition for the corresponding service implementation in that module and the app bundle would build without any errors.

Known workaround
I have not found any good workarounds yet.
As for now I define all service implementations in all modules and check if the implementations listed actually exist.
Maybe it would be possible to exclude the service definitions from the comparison done here:

private static void checkEqualEntries(ZipPath path, BundleModule module1, BundleModule module2) {
ModuleEntry entry1 = module1.getEntry(path).get();
ModuleEntry entry2 = module2.getEntry(path).get();
if (!entry1.equals(entry2)) {
throw ValidationException.builder()
.withMessage(
"Modules '%s' and '%s' contain entry '%s' with different content.",
module1.getName(), module2.getName(), path)
.build();
}
}

Environment:
OS: Mac OS Catalina as well as the docker image jangrewe/gitlab-ci-android in the newest version

When all the modules are installed on the device, all the files appear as a single APK to the Android platform, so when your app will try to read this file, the platform will arbitrarily load one file but you won't be able to predict which one. This is why we have this check in place.

rfha commented

Thanks for the fast response.

Would it make sense to add the option to define that specific files should be merged when building the app bundle? For example:

android{
    bundle{
        packagingOptions {
            merge 'root/META-INF/services/com.package.MyService'
            }
        }
    }
}

Do you pull these files from a third party library or SDK? Or where do these files come from? Could you simply exclude one of them from one of the library instead if you don't need it?

rfha commented

What I am trying to do is build dynamic feature modules that behave similar to java plugins loaded with the ServiceLoader class.
The idea is that the base module does not have to know each module but just call the ServiceLoader on each module.
For convenience I use the Google AutoService annotation so I don't have to create the META-INF/services files manually.
What I am doing at the moment to fix this error is compiling all modules, then merging the files with a short gradle script and then building the bundle. The problem is that I now can't use the ServiceProvider class because it fails if a class specified in the service definition does not exist. Because of that I also implemented a custom ServiceLoader that checks if the class exists before loading it.

It looks like you're creating two implementations with the same FQCN for an interface.
In which case the compiler is correct to throw an error.
What's your reason for not using a different classpath for these?

rfha commented

The file in META-INF/services is not named after the FQCN of the implementation but the FQCN of the Interface. The error is not thrown by the compiler but bundle tool.

Now I'm with you on this. Yes, it looks like merge should be able to do the trick here.

I had a similar error: Modules 'base' and 'tap_on_phone_dynamic_feature' contain entry 'root/META-INF/services/io.jsonwebtoken.io.Deserializer' with different content.
The service in the error is registered through the io.jsonwebtoken:jjwt-orgjon:0.11.0 artifact, which I in turn depend on only from my dynamic feature.

In my case the cause of this was when the base app module had a build type with minification (R8 enabled) that was not present in the dynamic feature module. i.e:

android {
    buildTypes {
        instrumentation {
            initWith debug
            minifyEnabled true
            matchingFallbacks = ['debug']
        }
    }
}

With minifyEnabled false this works fine - but without R8 seems to make some packaging errors which caused the service to be registered both in the base module and the dynamic feature module.

In order to fix it I had to add the following to build.gradle of the dynamic feature module:

android {
    buildTypes {
        instrumentation {
            initWith debug
            matchingFallbacks = ['debug']
        }
    }
}

Closing as no bundletool issue here, bundletool should not allow files with the same name but different content because as described all the files appear as a single APK to the Android platform.

The proposed solution, merging of provider-configuration files, can not be implemented in bundletool and relates to Android Gradle Plugin. Please create an issue in their issue tracker https://issuetracker.google.com/issues?q=componentid:192709 to get proper attention if this is still relevant. But even if implemented developers would still have to ensure that some implementations may be not available because they relate to on-demand features that are not installed on a device.