I'm trying to load a quantized graph into an Android app.
My BUILD file contains
deps = ["//tensorflow/core:android_tensorflow_lib",
"//tensorflow/contrib/quantization:cc_array_ops",
"//tensorflow/contrib/quantization:cc_math_ops",
"//tensorflow/contrib/quantization:cc_nn_ops",
"//tensorflow/contrib/quantization/kernels:quantized_ops"]
The additional quantization deps work for standalone C++ builds.
I can't compile with Bazel, due to a large number of errors in GEMMLOWP. What's the proper way to include gemmlowp and the quantization ops in Android?
Here is an example error:
external/gemmlowp/eight_bit_int_gemm/eight_bit_int_gemm.cc:125:13: error: 'int32_t' is not a member of 'std'
MatrixMap<std::int32_t, ResultOrder> result(c, m, n, ldc);
This is on Ubuntu 16.04 with Bazel 0.3.0.
Here's a gist that has the output of two sequential attempts to build the package - it fails on highwayhash the first time and gemmlowp the second.
https://gist.github.com/ericdanz/81b799f2e0bbb3cc462aa3c90468c71b
Ultimately got it to compile and run with liberal addition of "-std=c++11" in BUILD files for gemmlowp and highwayhash, and substitution of the android framework for the framework dependencies in the quantized ops. It produces fairly different results though, and runs about 4x slower (26-3200ms vs 6-800 ms). I'll try to do a little deeper investigation.
Here's what worked for me - it is basically a combination of all the comments from Eric D above, but I wanted to put it all in one place for someone new who comes across this problem:
Add quantized_ops in deps to libtensorflow_demo.so in the BUILD file for the Android app:
deps = ["//tensorflow/core:android_tensorflow_lib",
"//tensorflow/contrib/quantization/kernels:quantized_ops",]
Modify the deps for quantized_ops in tensorflow/contrib/quantization/kernels/BUILD:
deps = [
"//tensorflow/contrib/quantization:cc_array_ops",
"//tensorflow/contrib/quantization:cc_math_ops",
"//tensorflow/contrib/quantization:cc_nn_ops",
"//tensorflow/core:android_tensorflow_lib",
#"//tensorflow/core",
#"//tensorflow/core:framework",
#"//tensorflow/core:lib",
#"//tensorflow/core/kernels:concat_lib_hdrs",
#"//tensorflow/core/kernels:conv_ops",
#"//tensorflow/core/kernels:ops_util_hdrs",
#"//tensorflow/core/kernels:pooling_ops_hdrs",
#"//tensorflow/core/kernels:eigen_helpers",
#"//tensorflow/core/kernels:ops_util",
#"//tensorflow/core/kernels:pooling_ops",
"//third_party/eigen3",
"#gemmlowp//:eight_bit_int_gemm",
],
Remove/comment out the .Doc() parts in tensorflow/contrib/quantization/ops/array_ops.cc, math_ops.cc and nn_ops.cc
Modify the deps for cc_array_ops, cc_math_ops and cc_nn_ops in tensorflow/contrib/quantization/BUILD:
deps = [
#"//tensorflow/core:framework",
"//tensorflow/core:android_tensorflow_lib",
],
Run bazel build command for Android app with --cxxopt="-std=c++11" flag.
Related
I have a project that uses bazel and includes a demo Android app to show some functionality and widgets. I wanted to split this out as the base for a new demo/experimentation app to try out other widget combinations. So I copied the WORKSPACE file and the code from the demo app package into a new project (the package names changed but I modified the code and BUILD files to account for this). The original project builds just fine, but the new one consistently fails with the following error:
ERROR: D:/_bazel_out/s6wwoqrr/external/maven_android_everything/BUILD:568:11: Compiling Android resources for #maven_android_everything//:com
_google_android_material_material failed: (Exit 1): ResourceProcessorBusyBox.exe failed: error executing command bazel-out\x64_windows-opt-ex
ec-2B5CBBC6\bin\external\bazel_tools\src\tools\android\java\com\google\devtools\build\android\ResourceProcessorBusyBox.exe ... (remaining 1 a
rgument skipped)
Jul 29, 2022 3:04:29 PM java.util.stream.ForEachOps$ForEachOp$OfRef accept
SEVERE: Error during Compiling bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_everything\_aar\unzipped\resources\com_goog
le_android_material_material\res\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml:
Command: external\androidsdk\build-tools\33.0.0\aapt2.exe\
compile\
-v\
--legacy\
-o\
C:\Users\bdlei\AppData\Local\Temp\android_resources_tmp3839340977225638696\compiled\bazel-out\android-armeabi-v7a-fastbuild\bin\exter
nal\maven_android_everything\_aar\unzipped\resources\com_google_android_material_material\res\
bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_everything\_aar\unzipped\resources\com_google_android_material_mat
erial\res\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml
Output:
bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_everything\_aar\unzipped\resources\com_google_android_material_material\re
s\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml: error: file not found.
Exception in thread "main" com.google.devtools.build.android.aapt2.ResourceCompiler$CompileError
at com.google.devtools.build.android.aapt2.ResourceCompiler$CompileError.of(ResourceCompiler.java:111)
at com.google.devtools.build.android.aapt2.ResourceCompiler$CompilingVisitor.getCompiledArtifacts(ResourceCompiler.java:431)
at com.google.devtools.build.android.aapt2.ResourceCompiler.getCompiledArtifacts(ResourceCompiler.java:465)
at com.google.devtools.build.android.UnvalidatedAndroidData.compile(UnvalidatedAndroidData.java:103)
at com.google.devtools.build.android.CompileLibraryResourcesAction.main(CompileLibraryResourcesAction.java:143)
at com.google.devtools.build.android.ResourceProcessorBusyBox$Tool$6.call(ResourceProcessorBusyBox.java:99)
at com.google.devtools.build.android.ResourceProcessorBusyBox.processRequest(ResourceProcessorBusyBox.java:234)
at com.google.devtools.build.android.ResourceProcessorBusyBox.main(ResourceProcessorBusyBox.java:177)
Suppressed: java.lang.RuntimeException: Error during Compiling bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_eve
rything\_aar\unzipped\resources\com_google_android_material_material\res\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml:
Command: external\androidsdk\build-tools\33.0.0\aapt2.exe\
compile\
-v\
--legacy\
-o\
C:\Users\bdlei\AppData\Local\Temp\android_resources_tmp3839340977225638696\compiled\bazel-out\android-armeabi-v7a-fastbuild\bin\exter
nal\maven_android_everything\_aar\unzipped\resources\com_google_android_material_material\res\
bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_everything\_aar\unzipped\resources\com_google_android_material_mat
erial\res\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml
Output:
bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_everything\_aar\unzipped\resources\com_google_android_material_material\re
s\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml: error: file not found.
at com.google.devtools.build.android.CommandHelper.execute(CommandHelper.java:42)
at com.google.devtools.build.android.AaptCommandBuilder.execute(AaptCommandBuilder.java:297)
at com.google.devtools.build.android.aapt2.ResourceCompiler$CompileTask.compile(ResourceCompiler.java:234)
at com.google.devtools.build.android.aapt2.ResourceCompiler$CompileTask.call(ResourceCompiler.java:178)
at com.google.devtools.build.android.aapt2.ResourceCompiler$CompileTask.call(ResourceCompiler.java:125)
at com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListe
nableFutureTask.java:125)
at com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:69)
at com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:78)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
INFO: Elapsed time: 335.194s, Critical Path: 145.75s
INFO: 1568 processes: 74 internal, 1308 local, 186 worker.
FAILED: Build did NOT complete successfully
The referenced file: bazel-out\android-armeabi-v7a-fastbuild\bin\external\maven_android_everything\_aar\unzipped\resources\com_google_android_material_mat erial\res\animator\mtrl_extended_fab_change_size_collapse_motion_spec.xml does exist under the bazel-out directory of my project, but after the build failure, the temp directory C:\Users\bdlei\AppData\Local\Temp\android_resources_tmp3839340977225638696 does not exist. I had an explorer window open to my AppData\Local\Temp directory last time I tried building and many such android_resources_temp... came and went during the build (which I expected). I removed older ones before trying the build again and while I wasn't able to track if the one named in the error ever existed, it does not exist afterwards (exactly one of these was left after the build failure).
The behavior is consistent: the original project consistently succeeds and the branched out fails. This persists through bazel clean, bazel sync, and bazel shutdown calls.
Both projects are depending on android via a call into a 3rd "dependencies" project that creates a maven repo and builds a repo out of that:
def android():
if 'maven_android_everything' not in native.existing_rules():
maven_install(
name = "maven_android_everything",
artifacts = [
"androidx.activity:activity:1.2.3", # using 1.4.0 causes indexing issues in dex without force-jumbo, even without dagger-views.
"androidx.appcompat:appcompat:1.3.1",
"androidx.appcompat:appcompat-resources:1.3.1",
"androidx.constraintlayout:constraintlayout:2.1.1",
"androidx.core:core:1.7.0",
"androidx.fragment:fragment:1.3.6",
"androidx.lifecycle:lifecycle-viewmodel:2.3.1",
"androidx.viewpager2:viewpager2:1.0.0",
"androidx.recyclerview:recyclerview:1.0.0",
"androidx.tracing:tracing:1.0.0",
"com.google.android.material:material:1.4.0",
],
excluded_artifacts = [
maven.exclusion(
group = "com.google.guava",
artifact = "guava",
),
],
repositories = [
"https://maven.google.com",
"https://repo1.maven.org/maven2/",
],
)
new_simple_repo( # Simple custom repo rule to make an #android repo for use elsewhere
name = "android",
build_file_content = """
package(default_visibility = ["//visibility:public"])
android_library(
name = "android",
exports = [
"#guava//:concurrent",
"#maven_android_everything//:androidx_activity_activity",
"#maven_android_everything//:androidx_appcompat_appcompat",
"#maven_android_everything//:androidx_appcompat_appcompat_resources",
"#maven_android_everything//:androidx_constraintlayout_constraintlayout",
"#maven_android_everything//:androidx_core_core",
"#maven_android_everything//:androidx_fragment_fragment",
"#maven_android_everything//:androidx_lifecycle_lifecycle_viewmodel",
"#maven_android_everything//:androidx_viewpager2_viewpager2",
"#maven_android_everything//:androidx_recyclerview_recyclerview",
"#maven_android_everything//:com_google_android_material_material",
],
)
""")
I've tried bumping versions to see if maybe that might help, but after slogging through dependency conflicts and finally getting bazel sync to succeed, I hit this bug.
I'm trying to pare my "playgroung" project down and see if I can isolate anything, but even removing a dependency on the original project doesn't help.
I had some time to come back to this today and after much experimenting, including trying to set up a minimal example to use when filing a bug with bazel, I believe I found the problem. It appears to relate to the lengths of resulting file names. By trying various lengths of the name of the maven repo used in the maven_install command, I was able to get the build to fail or pass consistently based on the length. Any name 21 characters or shorter and the build succeeded, while any name longer than 21 characters would result in the file-not-found error.
Recently I upgrade my gradle from 6.5.1 to 7.0 i.e.
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
After the upgrade, I notice my test coverage dropped.
After investigation, I found out that in Gradle 6.5.1, my JaCoCo test coverage report shows some of my class has an encode() function, but it no longer exists in Gradle 7.0.
I dig out the simplest example below. The code is as below
In Gradle 6.5, my JaCoCo report is as below (notice there's an additional encode() function.
However, in Gradle 7.0, my JaCoCo report is as below
Because of this missing covered function, hence my coverage dropped. Nonetheless, it looks like in Gradle 7.0, that is more correct, as my real code doesn't have encode().
I'm just trying to understand where encode() function is there in the first place, and why it is no longer in Gradle 7.0? And is it correct that my assumption that Gradle 7.0 result is correct?
Different versions of Gradle come with different values of jacoco.toolVersion which you also can change in your build.gradle, and which controls version of JaCoCo to use.
Gradle 6.5 by default uses JaCoCo 0.8.5, and starting from Gradle 6.7 default is 0.8.6.
And here is a list of changes in JaCoCo version 0.8.6 - https://www.jacoco.org/jacoco/trunk/doc/changes.html
Most likely your interface ContextData contains method encode with default implementation.
JVM supports default methods in interfaces starting from version 8, however Kotlin supports compilation into bytecode of version 6. To do so Kotlin compiler does a trick for such methods - it generates method in classes implementing this interface that merely delegates execution to default implementation:
for the following Example.kt
interface ContextData {
fun encode() = { /* something */ }
}
data class SearchRefinementModalOpenData(
val userAction: String?
) : ContextData
execution of
kotlin-compiler-1.4.32/bin/kotlinc Example.kt
javap -v -p SearchRefinementModalOpenData.class
shows following bytecode
public final class SearchRefinementModalOpenData implements ContextData
{
public kotlin.jvm.functions.Function0<kotlin.Unit> encode();
descriptor: ()Lkotlin/jvm/functions/Function0;
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokestatic #29 // Method ContextData$DefaultImpls.encode:(LContextData;)Lkotlin/jvm/functions/Function0;
4: areturn
JaCoCo starting from version 0.8.6 filters out such methods since they are compiler artifacts and not presented in original source code.
It looks like you are using kotlinx.Serialization.
That generates a few encodeToString and similar functions for your objects. I guess Gradle 7 now ignores those?
I need to iterate over specific classes from main package in my android unit test, to check some of their properties.
For this I use standard approach, using ClassLoader:
val classLoader = Thread.currentThread().contextClassLoader
val resources: Enumeration<URL> = classLoader.getResources("com/models/package")
assert(resources.hasMoreElements()) // Fails from CL, works in AS
Before the Gradle update (had Gradle 5.6.4) that worked. Now the behaviour is as follows: it works when test is run from Android Studio, but fails (returns empty enumeration) when run from command line with gradlew.
I wonder what might be the difference in this respect between the two Gradle versions? And why it still works when run from Studio?
Some considerations and things I have tried:
Referencing these classes in unit test works ok, and also classLoader.findClass("com.models.package.MyModel") and
classLoader.loadClass("com.models.package.MyModel") from unit test is working. But even after that classLoader.getResources("com/models/package") returns empty enumeration.
Using other references to ClassLoader, like MyModel::class.java.classLoader and ClassLoader.getSystemClassLoader() didn't make any difference.
Gradle build from command line contains the warning "OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended", but as far as I can tell it's not connected to my issue.
If I put some of the classes from 'com/models/package' to the unit test /test folder, they are getting returned in enumeration.
This might be connected with some new optimisation setting that makes ClassLoaders omit registering some of the classes, in different root directories, but as it still works in AS there might be some setting to turn this optimisation off in a command line build also?
Thank you for any suggestions on this.
In Gradle 6.7.1 I had to include the directory with the code to the test sourceSets. Afterwards the classloader from junit started to see the classes and return them in Enumeration.
sourceSets {
test {
java.srcDirs += ['src/main']
}
}
Using Ibotta, an AOP gradle plugin for android, seems not working after adding below dependency
implementation 'com.google.firebase:firebase-auth:20.0.3'
Actual Result:
Build was successful and seems not creating all dex
Runtime Error:
Application class not found in dex path list.
AOP Log showing below error :
java.lang.IllegalStateException: Expecting .,<, or ;, but found - while unpacking <MessageType:Lcom/google/android/gms/internal/firebase-auth-api/zzaaa<TMessageType;TBuilderType;>;BuilderType:Lcom/google/android/gms/internal/firebase-auth-api/zzzw<TMessageType;TBuilderType;>;>Lcom/google/android/gms/internal/firebase-auth-api/zzyj<TMessageType;TBuilderType;>;
at org.aspectj.util.GenericSignatureParser.parseClassTypeSignature(GenericSignatureParser.java:204)
at org.aspectj.util.GenericSignatureParser.parseFieldTypeSignature(GenericSignatureParser.java:155)
at org.aspectj.util.GenericSignatureParser.parseFormalTypeParameter(GenericSignatureParser.java:130)
at org.aspectj.util.GenericSignatureParser.parseAsClassSignature(GenericSignatureParser.java:51)
at org.aspectj.weaver.UnresolvedType.forGenericTypeSignature(UnresolvedType.java:275)
at org.aspectj.weaver.bcel.BcelWorld.addSourceObjectType(BcelWorld.java:479)
at org.aspectj.weaver.bcel.BcelWorld.addSourceObjectType(BcelWorld.java:453)
at org.aspectj.weaver.bcel.BcelWeaver.addAspectsFromJarFile(BcelWeaver.java:265)
at org.aspectj.weaver.bcel.BcelWeaver.addLibraryJarFile(BcelWeaver.java:238)
Below added the required code to reproduce this.
gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
project build.gradle
repositories {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "com.ibotta:plugin:1.1.0"
}
app build.gradle
apply plugin: "com.ibotta.gradle.aop"
dependencies {
//AspectJ
implementation "org.aspectj:aspectjrt:1.9.6"
implementation 'com.google.firebase:firebase-auth:20.0.3'
}
AspectLogging class
#Aspect
class AOPLog {
#Pointcut("within(com.example.aopdemo..*) && execution(* *(..))")
fun allMethods() {
}
#Around("allMethods()")
#Throws(Throwable::class)
open fun onMethodAdvice(joinPoint: JoinPoint?): Any? {
val methodSignature = joinPoint!!.signature as MethodSignature
val methodName = methodSignature.name
val startTime = System.currentTimeMillis();
val result = (joinPoint as ProceedingJoinPoint).proceed()
val endTime = System.currentTimeMillis() - startTime
Log.e("LoggingVM", "$methodName ---> $endTime")
return result
}
}
Note: Firebase dependency is just added and not using anywhere in the demo app.
com.example.aopdemo package just have a launcher activity, and invoking some methods on onCreate method. It was tracking all the methods before firebase added. After adding, getting above errors.
Using Ibotta, an AOP library for android
It is not an AOP library but a Gradle plugin for people wishing to weave AspectJ aspects into their Android target apps or libraries, see here.
What strikes me as odd in your error message is the package name com/google/android/gms/internal/firebase-auth-api/zzaaa - please note the hyphens. Actually, hyphen ("-") characters in package names are illegal, see Java naming conventions.
If Firebase itself or maybe some kind of code converter or obfuscator uses firebase-auth-api instead of something like firebase_auth_api or firebase.auth.api (depending on what it looks like in the original code base), it is no surprise at all that it derails the AspectJ signature parser. I wonder which compiler even permits that nowadays.
Fix the package name, then I guess the AspectJ problem will disappear too. The error reported by AspectJ is valid, the problem is outside of AspectJ.
Update: I looked at the AAR file you added to your class path and indeed it contains an illegal package name with obfuscated classes, probably on purpose in order to make it more difficult to work with and manipulate those classes:
$ unzip firebase-auth-20.0.3.aar -d firebase-auth
Archive: firebase-auth-20.0.3.aar
inflating: firebase-auth/AndroidManifest.xml
inflating: firebase-auth/R.txt
inflating: firebase-auth/classes.jar
inflating: firebase-auth/proguard.txt
inflating: firebase-auth/third_party_licenses.json
inflating: firebase-auth/third_party_licenses.txt
$ unzip -l firebase-auth/classes.jar | grep firebase-auth-api | head -n 10
1209 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zza.class
1032 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaa.class
10997 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaaa.class
4291 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaab.class
172 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaac.class
275 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaad.class
192 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaae.class
525 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaaf.class
2430 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaag.class
373 2021-02-26 09:54 com/google/android/gms/internal/firebase-auth-api/zzaah.class
Unfortunately, this also stops legal use cases from working. You may want to raise an issue or alternatively make sure to exclude Firebase or at least its internal packages when using AspectJ, e.g. via !within(com.google.android.gms.internal..*). Or do your aspects actually want to modify anything in Firebase?
Update 2: Inside Ibotta, there is a source code comment as follows:
A Gradle plugin that performs AOP weaving using a technique recommended by Gradle reps. It taps into Android's bytecode manipulation pipeline which is a far more logical approach.
The basic idea is:
Change the Kotlin and Java compile output directories.
Copy the Kotlin/Java compiled output to one directory.
Weave the combined Kotlin/Java classes.
Be registered as a bytecode generator so that Android recognizes the custom AOP weaving as a formal step in the build pipeline.
Maybe in using this approach instead of calling the stand-alone AspectJ compiler, somehow the byte code weaver has to load and see all classes, even the ones the normal AspectJ compiler would ignore due to the within(com.example.aopdemo..*) in your sample aspect or the safe-guarding !within(com.google.android.gms.internal..*) I suggested. Either way, that it works after you changed the Gradle build plugin tells me that you should raise an issue for the Ibotta project (not for Firebase like I initially thought). Actually, it seems that you already did that, see issue #5.
Update 3: A while ago, the Ibotta maintainer said that you should use the tool's own filtering feature and use something like
aopWeave {
filter = "com/example/aopdemo"
}
TL;DR;
How to add two or more kotlin native modules on an iOS project without getting duplicate symbols error?
The detailed question
Let's assume a multi-module KMP project as a follow where there exists a native app for Android and a native app for iOS and two common modules to hold shared kotlin code.
.
├── android
│ └── app
├── common
│ ├── moduleA
│ └── moduleB
├── ios
│ └── app
The module A contains a data class HelloWorld and has no module dependencies:
package hello.world.modulea
data class HelloWorld(
val message: String
)
Module B contains an extension function for HelloWorld class so it depends on module A:
package hello.world.moduleb
import hello.world.modulea.HelloWorld
fun HelloWorld.egassem() = message.reversed()
The build.gradle configuration of the modules are:
Module A
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"
…
kotlin {
targets {
jvm("android")
def iosClosure = {
binaries {
framework("moduleA")
}
}
if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
}
cocoapods {…}
sourceSets {
commonMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
}
androidMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
}
iosMain.dependencies {
}
}
}
Module B
apply plugin: "org.jetbrains.kotlin.multiplatform"
apply plugin: "org.jetbrains.kotlin.native.cocoapods"
…
kotlin {
targets {
jvm("android")
def iosClosure = {
binaries {
framework("moduleB")
}
}
if (System.getenv("SDK_NAME")?.startsWith("iphoneos")) {…}
}
cocoapods {…}
sourceSets {
commonMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:1.3.72"
implementation project(":common:moduleA")
}
androidMain.dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
}
iosMain.dependencies {
}
}
}
It looks pretty straightforward and it even works on android if I configure the android build gradle dependencies as a following:
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72"
implementation project(":common:moduleA")
implementation project(":common:moduleB")
}
However, this does not seem to be the correct way to organize multi modules on iOS, because running the ./gradlew podspec I get a BUILD SUCCESSFUL as expected with the following pods:
pod 'moduleA', :path => '…/HelloWorld/common/moduleA'
pod 'moduleB', :path => '…/HelloWorld/common/moduleB'
Even running a pod install I get a success output Pod installation complete! There are 2 dependencies from the Podfile and 2 total pods installed. whats looks correctly once the Xcode shows the module A and module B on the Pods section.
However, if I try to build the iOS project I get the following error:
Ld …/Hello_World-…/Build/Products/Debug-iphonesimulator/Hello\ World.app/Hello\ World normal x86_64 (in target 'Hello World' from project 'Hello World')
cd …/HelloWorld/ios/app
…
duplicate symbol '_ktypew:kotlin.Any' in:
…/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
…/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
… a lot of duplicate symbol more …
duplicate symbol '_kfun:kotlin.throwOnFailure$stdlib#kotlin.Result<#STAR>.()' in:
…/HelloWorld/common/moduleA/build/cocoapods/framework/moduleA.framework/moduleA(result.o)
…/HelloWorld/common/moduleB/build/cocoapods/framework/moduleB.framework/moduleB(result.o)
ld: 9928 duplicate symbols for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
My knowledge in iOS is not that much, so to my untrained eyes, it looks like each module is adding its own version of the things instead of using some resolutions strategy to share it.
If I use only the module A the code works and run as expected, so I know the code itself is correct, the problem is how to manage more than 1 module, so that the question, how to add both (module A and module B) on iOS and make things works?
P.S
I did reduce the code as much as I could, trying to keep only the parts that I guess is the source of the problem, however, the complete code is available here if you want to check anything missing in the snippets, or if you want to run and try to solve the problem…
Multiple Kotlin frameworks can be tricky, but should be working as of 1.3.70 which I see you have.
The issue seems to be that both frameworks are static, which is currently an issue in 1.3.70 so it isn't working. (This should be updated by 1.40). It looks like by default the cocoapods plugin sets the frameworks to be static which won't work. I'm unaware of a way to change cocoapods to set it as dynamic but I've tested building without cocoapods and using the isStatic variable in a gradle task, and have gotten an iOS project to compile. Something like:
binaries {
framework("moduleA"){
isStatic = false
}
}
For now you can work around the issue using this method by using the code above and creating a task to build the frameworks(here's an example)
Another thing worth noting is that on the iOS side, the HelloWorld classes will appear as two separate classes despite both coming from moduleA. It's another strange situation with multiple Kotlin frameworks, but I think the extension will still work in this case since you're returning a string.
I actually just wrote up a blog post about multiple Kotlin frameworks that may help with some other questions if you'd like to take a look. https://touchlab.co/multiple-kotlin-frameworks-in-application/
EDIT: Looks like cocoapodsext also has an isStatic variable, so set it to isStatic = false
tl:dr You currently can't have more than one static Kotlin frameworks in the same iOS project. Set them to not be static using isStatic = false.
However, if I try to build the iOS project I get the following error:
This particular error is a known issue. Multiple debug static frameworks are incompatible with compiler caches.
So to workaround the issue you can either disable compiler caches by putting the following line into your gradle.properties:
kotlin.native.cacheKind=none
or make the frameworks dynamic by adding the following snippet to your Gradle build script:
kotlin {
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
binaries.withType<org.jetbrains.kotlin.gradle.plugin.mpp.Framework> {
isStatic = false
}
}
}
See https://youtrack.jetbrains.com/issue/KT-42254 for more details.
I guess current behaviour for multiple frameworks doesn't make much sense for the original topic starter, I'm just putting my answer here for anyone who might encounter the same issue.
My knowledge in iOS is not that much, so to my untrained eyes, it looks like each module is adding its own version of the things instead of using some resolutions strategy to share it.
This is exactly how it is supposed to work at this moment. But "versions of the things" in each of the frameworks are put into the separate independent namespaces, so there should be no linkage errors, and the one you've encountered is a bug.