How to assemble an Android Program
Those notes are for creating an android program with Java, Kotlin, and C++ in CMake, without Gradle.
Why
The official tool for creating android packages is the build system Gradle.
Unfortunately I really dislike it. I admit that I do not know the build system very well, but in my relatively limited experience with it, I’ve noticed following issues:
-
breaking changes between releases
-
long packaging times
-
leads to mixture of build systems when packaging native code
-
inscrutable error messages
-
breaking changes between releases
-
creating different flavours with Gradle dimension quickly gets out of hand
-
no way to download dependencies before building
-
Gradle might delete downloaded dependencies 🗄️, no way to pin them
-
sharing the cache between concurrent builds (which is the default behaviour) does not work 🗄️
-
unclear which android SDK is used (android studio insists I should use the one of android studio instead of the one configured in the project; why?)
-
unclear which java version is used (since Gradle needs a Java instance for being executed)
-
breaking changes between releases
-
The official IDE (Android Studio) consumes a lot of resources just for opening a project
-
long loading times in Android Studio because of "gradle sync" (without further details). Even if the project can be built immediately from the command line, sometime Android Studio wants to download something
-
it is too easy to pull too many dependencies
-
too many issues on the internet are resolved with "cleaned the project", updating to a newer version, or (what matches my experience) clean the
~/.gradle
folder
Granted, not all issues are specific to Gradle (IDE and interaction with other languages), and some issues can surely be attributed to my lack of knowledge.
I guess I should explain further about some points.
An example of issue solved by nuking the ~/.gradle
folder:
e: /home/build/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/logging-interceptor/5.0.0-alpha.12/39e57bab216d200b965ff60723486d06d89dd6e4/logging-interceptor-5.0.0-alpha.12.jar!/META-INF/logging-interceptor.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: /home/build/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp-jvm/5.0.0-alpha.12/b54e5430568c6210032f1d03c99cda3db01da3b5/okhttp-jvm-5.0.0-alpha.12.jar!/META-INF/okhttp.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: /home/build/.gradle/caches/modules-2/files-2.1/com.squareup.okio/okio-jvm/3.7.0/276b999b41f7dcde00054848fc53af338d86b349/okio-jvm-3.7.0.jar!/META-INF/okio.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: /home/build/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.9.21/17ee3e873d439566c7d8354403b5f3d9744c4c9c/kotlin-stdlib-1.9.21.jar!/META-INF/kotlin-stdlib.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: /home/build/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.9.21/17ee3e873d439566c7d8354403b5f3d9744c4c9c/kotlin-stdlib-1.9.21.jar!/META-INF/kotlin-stdlib-jdk8.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: /home/build/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.9.21/17ee3e873d439566c7d8354403b5f3d9744c4c9c/kotlin-stdlib-1.9.21.jar!/META-INF/kotlin-stdlib-jdk7.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
Why is Gradle picking a wrong module?
The directory names are unreadable because Gradle hashes all dependencies, this should ensure that multiple dependencies (and multiple versions of the same dependency) can be handled correctly. I seriously doubt that the package kotlin-stdlib
managed to create a collision.
An example of error message; the root cause is a typo in a manifest file. But note that the xml file is still "valid", there where no unclosed quotes or something that could confuse a parser; I wrote accidentally applicationa
instead of application
.
The error message is over 200 lines of text, and only one line of text (the actual useful line) come from the tool used for packaging the application, everything else is just noise:
Show error message
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':xxx:yyy'.
> A failure occurred while executing com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction
> Android resource linking failed
ERROR: C:\project\AndroidAppxxx\xxx\src\AndroidManifest.xml:6:2-10:4: AAPT: error: unexpected element <applicationa> found in <manifest>.
* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':xxx:yyy'.
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:130)
at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:293)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:128)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
Caused by: org.gradle.workers.internal.DefaultWorkerExecutor$WorkExecutionException: A failure occurred while executing com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction
at org.gradle.workers.internal.DefaultWorkerExecutor$WorkItemExecution.waitForCompletion(DefaultWorkerExecutor.java:287)
at org.gradle.internal.work.DefaultAsyncWorkTracker.lambda$waitForItemsAndGatherFailures$2(DefaultAsyncWorkTracker.java:130)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLocks(DefaultWorkerLeaseService.java:339)
at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLocks(DefaultWorkerLeaseService.java:322)
at org.gradle.internal.work.DefaultWorkerLeaseService.withoutLock(DefaultWorkerLeaseService.java:327)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForItemsAndGatherFailures(DefaultAsyncWorkTracker.java:126)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForItemsAndGatherFailures(DefaultAsyncWorkTracker.java:92)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForAll(DefaultAsyncWorkTracker.java:78)
at org.gradle.internal.work.DefaultAsyncWorkTracker.waitForCompletion(DefaultAsyncWorkTracker.java:66)
at org.gradle.api.internal.tasks.execution.TaskExecution$3.run(TaskExecution.java:252)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeAction(TaskExecution.java:229)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeActions(TaskExecution.java:212)
at org.gradle.api.internal.tasks.execution.TaskExecution.executeWithPreviousOutputFiles(TaskExecution.java:195)
at org.gradle.api.internal.tasks.execution.TaskExecution.execute(TaskExecution.java:162)
at org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)
at org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)
at org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)
at org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)
at org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:42)
at org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:75)
at org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)
at org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:67)
at org.gradle.internal.execution.steps.RemovePreviousOutputsStep.execute(RemovePreviousOutputsStep.java:37)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)
at org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:69)
at org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:46)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:40)
at org.gradle.internal.execution.steps.ResolveInputChangesStep.execute(ResolveInputChangesStep.java:29)
at org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)
at org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$1(BuildCacheStep.java:75)
at org.gradle.internal.Either$Right.fold(Either.java:175)
at org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)
at org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:46)
at org.gradle.internal.execution.steps.StoreExecutionStateStep.execute(StoreExecutionStateStep.java:35)
at org.gradle.internal.execution.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:75)
at org.gradle.internal.execution.steps.SkipUpToDateStep.lambda$execute$2(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:53)
at org.gradle.internal.execution.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:35)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:49)
at org.gradle.internal.execution.steps.ResolveIncrementalCachingStateStep.executeDelegate(ResolveIncrementalCachingStateStep.java:27)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)
at org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:65)
at org.gradle.internal.execution.steps.ResolveChangesStep.execute(ResolveChangesStep.java:36)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:105)
at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:54)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:64)
at org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.executeWithNonEmptySources(AbstractSkipEmptyWorkStep.java:125)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:56)
at org.gradle.internal.execution.steps.AbstractSkipEmptyWorkStep.execute(AbstractSkipEmptyWorkStep.java:36)
at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:36)
at org.gradle.internal.execution.steps.LoadPreviousExecutionStateStep.execute(LoadPreviousExecutionStateStep.java:23)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:75)
at org.gradle.internal.execution.steps.HandleStaleOutputsStep.execute(HandleStaleOutputsStep.java:41)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.lambda$execute$0(AssignMutableWorkspaceStep.java:35)
at org.gradle.api.internal.tasks.execution.TaskExecution$4.withWorkspace(TaskExecution.java:289)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:31)
at org.gradle.internal.execution.steps.AssignMutableWorkspaceStep.execute(AssignMutableWorkspaceStep.java:22)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:40)
at org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)
at org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)
at org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:48)
at org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:35)
at org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:61)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:127)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
Caused by: com.android.builder.internal.aapt.v2.Aapt2Exception: Android resource linking failed
ERROR: C:\project\AndroidAppxxx\xxx\src\AndroidManifest.xml:6:2-10:4: AAPT: error: unexpected element <applicationa> found in <manifest>.
at com.android.builder.internal.aapt.v2.Aapt2Exception$Companion.create(Aapt2Exception.kt:45)
at com.android.builder.internal.aapt.v2.Aapt2Exception$Companion.create$default(Aapt2Exception.kt:33)
at com.android.build.gradle.internal.res.Aapt2ErrorUtils.rewriteException(Aapt2ErrorUtils.kt:262)
at com.android.build.gradle.internal.res.Aapt2ErrorUtils.rewriteLinkException(Aapt2ErrorUtils.kt:133)
at com.android.build.gradle.internal.res.Aapt2ProcessResourcesRunnableKt.processResources(Aapt2ProcessResourcesRunnable.kt:76)
at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$Companion.invokeAaptForSplit(LinkApplicationAndroidResourcesTask.kt:945)
at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$Companion.access$invokeAaptForSplit(LinkApplicationAndroidResourcesTask.kt:799)
at com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction.run(LinkApplicationAndroidResourcesTask.kt:432)
at com.android.build.gradle.internal.profile.ProfileAwareWorkAction.execute(ProfileAwareWorkAction.kt:74)
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:195)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:128)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:170)
at org.gradle.internal.Factories$1.create(Factories.java:31)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:131)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:136)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:165)
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:134)
... 2 more
While in this case what is causing the error is clear (ERROR: C:\project\AndroidAppxxx\xxx\src\AndroidManifest.xml:6:2-10:4: AAPT: error: unexpected element <applicationa> found in <manifest>.
), one has first to find it. In comparison, linker errors in C++, or errors caused by templates, are a breeze of fresh air!
What about custom errors?
For example, cosndier following snippet in a gradle file
if ( System.getenv('MY_ENV_VAR') == null ) {
throw new GradleException("Environment variable MY_ENV_VAR is not set, you will not be able to download artifact from the repository")
}
This is the generated output
Details
FAILURE: Build failed with an exception.
* Where:
Script 'D:\project\custom-logic.gradle' line: 16
* What went wrong:
A problem occurred evaluating script.
> Environment variable MY_ENV_VAR is not set, you will not be able to download artifact from the repository
* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
* Exception is:
org.gradle.api.GradleScriptException: A problem occurred evaluating script.
at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:93)
at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.lambda$apply$0(DefaultScriptPluginFactory.java:137)
at org.gradle.configuration.DefaultScriptTarget.addConfiguration(DefaultScriptTarget.java:74)
at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:140)
at org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:68)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.configuration.BuildOperationScriptPlugin.lambda$apply$0(BuildOperationScriptPlugin.java:65)
at org.gradle.internal.code.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:44)
at org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:65)
at org.gradle.api.internal.plugins.DefaultObjectConfigurationAction.applyScript(DefaultObjectConfigurationAction.java:150)
at org.gradle.api.internal.plugins.DefaultObjectConfigurationAction.access$000(DefaultObjectConfigurationAction.java:43)
at org.gradle.api.internal.plugins.DefaultObjectConfigurationAction$1.run(DefaultObjectConfigurationAction.java:76)
at org.gradle.api.internal.plugins.DefaultObjectConfigurationAction.execute(DefaultObjectConfigurationAction.java:184)
at org.gradle.api.internal.project.AbstractPluginAware.apply(AbstractPluginAware.java:49)
at org.gradle.api.internal.project.ProjectScript.apply(ProjectScript.java:37)
at org.gradle.api.Script$apply$0.callCurrent(Unknown Source)
at build_d28ijn3udemktcv0w8t5doyud.run(D:\project\release\build.gradle:216)
at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:91)
at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.lambda$apply$0(DefaultScriptPluginFactory.java:137)
at org.gradle.configuration.ProjectScriptTarget.addConfiguration(ProjectScriptTarget.java:79)
at org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:140)
at org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:68)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.configuration.BuildOperationScriptPlugin.lambda$apply$0(BuildOperationScriptPlugin.java:65)
at org.gradle.internal.code.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:44)
at org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:65)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:407)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:425)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:406)
at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:46)
at org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:27)
at org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:35)
at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.lambda$run$0(LifecycleProjectEvaluator.java:109)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:407)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$fromMutableState$2(DefaultProjectStateRegistry.java:430)
at org.gradle.internal.work.DefaultWorkerLeaseService.withReplacedLocks(DefaultWorkerLeaseService.java:363)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:430)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:406)
at org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.run(LifecycleProjectEvaluator.java:100)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:72)
at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:756)
at org.gradle.api.internal.project.DefaultProject.evaluate(DefaultProject.java:157)
at org.gradle.api.internal.project.ProjectLifecycleController.lambda$ensureSelfConfigured$2(ProjectLifecycleController.java:84)
at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254)
at org.gradle.internal.model.StateTransitionController.lambda$maybeTransitionIfNotCurrentlyTransitioning$10(StateTransitionController.java:199)
at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:34)
at org.gradle.internal.model.StateTransitionController.maybeTransitionIfNotCurrentlyTransitioning(StateTransitionController.java:195)
at org.gradle.api.internal.project.ProjectLifecycleController.ensureSelfConfigured(ProjectLifecycleController.java:84)
at org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.ensureConfigured(DefaultProjectStateRegistry.java:381)
at org.gradle.execution.TaskPathProjectEvaluator.configure(TaskPathProjectEvaluator.java:34)
at org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:50)
at org.gradle.configuration.DefaultProjectsPreparer.prepareProjects(DefaultProjectsPreparer.java:42)
at org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:65)
at org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
at org.gradle.configuration.BuildOperationFiringProjectsPreparer.prepareProjects(BuildOperationFiringProjectsPreparer.java:40)
at org.gradle.initialization.VintageBuildModelController.lambda$prepareProjects$2(VintageBuildModelController.java:84)
at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254)
at org.gradle.internal.model.StateTransitionController.lambda$transitionIfNotPreviously$11(StateTransitionController.java:213)
at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:34)
at org.gradle.internal.model.StateTransitionController.transitionIfNotPreviously(StateTransitionController.java:209)
at org.gradle.initialization.VintageBuildModelController.prepareProjects(VintageBuildModelController.java:84)
at org.gradle.initialization.VintageBuildModelController.prepareToScheduleTasks(VintageBuildModelController.java:71)
at org.gradle.internal.build.DefaultBuildLifecycleController.lambda$prepareToScheduleTasks$6(DefaultBuildLifecycleController.java:175)
at org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254)
at org.gradle.internal.model.StateTransitionController.lambda$maybeTransition$9(StateTransitionController.java:190)
at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:34)
at org.gradle.internal.model.StateTransitionController.maybeTransition(StateTransitionController.java:186)
at org.gradle.internal.build.DefaultBuildLifecycleController.prepareToScheduleTasks(DefaultBuildLifecycleController.java:173)
at org.gradle.internal.buildtree.DefaultBuildTreeWorkPreparer.scheduleRequestedTasks(DefaultBuildTreeWorkPreparer.java:36)
at org.gradle.internal.cc.impl.VintageBuildTreeWorkController$scheduleAndRunRequestedTasks$1.apply(VintageBuildTreeWorkController.kt:36)
at org.gradle.internal.cc.impl.VintageBuildTreeWorkController$scheduleAndRunRequestedTasks$1.apply(VintageBuildTreeWorkController.kt:35)
at org.gradle.composite.internal.DefaultIncludedBuildTaskGraph.withNewWorkGraph(DefaultIncludedBuildTaskGraph.java:112)
at org.gradle.internal.cc.impl.VintageBuildTreeWorkController.scheduleAndRunRequestedTasks(VintageBuildTreeWorkController.kt:35)
at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$scheduleAndRunTasks$1(DefaultBuildTreeLifecycleController.java:77)
at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$runBuild$4(DefaultBuildTreeLifecycleController.java:120)
at org.gradle.internal.model.StateTransitionController.lambda$transition$6(StateTransitionController.java:169)
at org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)
at org.gradle.internal.model.StateTransitionController.lambda$transition$7(StateTransitionController.java:169)
at org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:44)
at org.gradle.internal.model.StateTransitionController.transition(StateTransitionController.java:169)
at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.runBuild(DefaultBuildTreeLifecycleController.java:117)
at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.scheduleAndRunTasks(DefaultBuildTreeLifecycleController.java:77)
at org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.scheduleAndRunTasks(DefaultBuildTreeLifecycleController.java:72)
at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:31)
at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
at org.gradle.internal.buildtree.ProblemReportingBuildActionRunner.run(ProblemReportingBuildActionRunner.java:49)
at org.gradle.launcher.exec.BuildOutcomeReportingBuildActionRunner.run(BuildOutcomeReportingBuildActionRunner.java:65)
at org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner.run(FileSystemWatchingBuildActionRunner.java:140)
at org.gradle.launcher.exec.BuildCompletionNotifyingBuildActionRunner.run(BuildCompletionNotifyingBuildActionRunner.java:41)
at org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.lambda$execute$0(RootBuildLifecycleBuildActionExecutor.java:40)
at org.gradle.composite.internal.DefaultRootBuildState.run(DefaultRootBuildState.java:130)
at org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.execute(RootBuildLifecycleBuildActionExecutor.java:40)
at org.gradle.internal.buildtree.InitDeprecationLoggingActionExecutor.execute(InitDeprecationLoggingActionExecutor.java:62)
at org.gradle.internal.buildtree.InitProblems.execute(InitProblems.java:36)
at org.gradle.internal.buildtree.DefaultBuildTreeContext.execute(DefaultBuildTreeContext.java:40)
at org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.lambda$execute$0(BuildTreeLifecycleBuildActionExecutor.java:71)
at org.gradle.internal.buildtree.BuildTreeState.run(BuildTreeState.java:60)
at org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.execute(BuildTreeLifecycleBuildActionExecutor.java:71)
at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$3.call(RunAsBuildOperationBuildActionExecutor.java:61)
at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$3.call(RunAsBuildOperationBuildActionExecutor.java:57)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
at org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor.execute(RunAsBuildOperationBuildActionExecutor.java:57)
at org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.lambda$execute$0(RunAsWorkerThreadBuildActionExecutor.java:36)
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:267)
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:131)
at org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.execute(RunAsWorkerThreadBuildActionExecutor.java:36)
at org.gradle.tooling.internal.provider.continuous.ContinuousBuildActionExecutor.execute(ContinuousBuildActionExecutor.java:110)
at org.gradle.tooling.internal.provider.SubscribableBuildActionExecutor.execute(SubscribableBuildActionExecutor.java:64)
at org.gradle.internal.session.DefaultBuildSessionContext.execute(DefaultBuildSessionContext.java:46)
at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor$ActionImpl.apply(BuildSessionLifecycleBuildActionExecutor.java:92)
at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor$ActionImpl.apply(BuildSessionLifecycleBuildActionExecutor.java:80)
at org.gradle.internal.session.BuildSessionState.run(BuildSessionState.java:71)
at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor.execute(BuildSessionLifecycleBuildActionExecutor.java:62)
at org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor.execute(BuildSessionLifecycleBuildActionExecutor.java:41)
at org.gradle.internal.buildprocess.execution.StartParamsValidatingActionExecutor.execute(StartParamsValidatingActionExecutor.java:64)
at org.gradle.internal.buildprocess.execution.StartParamsValidatingActionExecutor.execute(StartParamsValidatingActionExecutor.java:32)
at org.gradle.internal.buildprocess.execution.SessionFailureReportingActionExecutor.execute(SessionFailureReportingActionExecutor.java:51)
at org.gradle.internal.buildprocess.execution.SessionFailureReportingActionExecutor.execute(SessionFailureReportingActionExecutor.java:39)
at org.gradle.internal.buildprocess.execution.SetupLoggingActionExecutor.execute(SetupLoggingActionExecutor.java:47)
at org.gradle.internal.buildprocess.execution.SetupLoggingActionExecutor.execute(SetupLoggingActionExecutor.java:31)
at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:70)
at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:39)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:29)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:35)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.ForwardClientInput.lambda$execute$0(ForwardClientInput.java:40)
at org.gradle.internal.daemon.clientinput.ClientInputForwarder.forwardInput(ClientInputForwarder.java:80)
at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:37)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:53)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:63)
at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:84)
at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)
at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)
at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:52)
at org.gradle.launcher.daemon.server.DaemonStateCoordinator.lambda$runCommand$0(DaemonStateCoordinator.java:320)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
Caused by: org.gradle.api.GradleException: Environment variable MY_ENV_VAR is not set, you will not be able to download artifact from the repository
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at org.gradle.internal.classpath.intercept.DefaultCallSiteDecorator$DecoratingCallSite.access$401(DefaultCallSiteDecorator.java:145)
at org.gradle.internal.classpath.intercept.DefaultCallSiteDecorator$DecoratingCallSite$4.callOriginal(DefaultCallSiteDecorator.java:199)
at org.gradle.internal.classpath.intercept.DefaultCallSiteDecorator$1.intercept(DefaultCallSiteDecorator.java:63)
at org.gradle.internal.classpath.intercept.DefaultCallSiteDecorator$DecoratingCallSite.callConstructor(DefaultCallSiteDecorator.java:196)
at extensionbuild_24upet4amd11vb72gcdt4o5ix$_run_closure1.doCall$original(D:\project\custom-logic.gradle:16)
at extensionbuild_24upet4amd11vb72gcdt4o5ix$_run_closure1.doCall(D:\project\custom-logic.gradle)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at org.gradle.util.internal.ClosureBackedAction.execute(ClosureBackedAction.java:73)
at org.gradle.util.internal.ConfigureUtil.configureTarget(ConfigureUtil.java:166)
at org.gradle.util.internal.ConfigureUtil.configure(ConfigureUtil.java:107)
at org.gradle.api.internal.project.DefaultProject.dependencies(DefaultProject.java:1267)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at org.gradle.internal.metaobject.BeanDynamicObject$MetaClassAdapter.invokeMethod(BeanDynamicObject.java:547)
at org.gradle.internal.metaobject.BeanDynamicObject.tryInvokeMethod(BeanDynamicObject.java:218)
at org.gradle.internal.metaobject.CompositeDynamicObject.tryInvokeMethod(CompositeDynamicObject.java:99)
at org.gradle.internal.extensibility.MixInClosurePropertiesAsMethodsDynamicObject.tryInvokeMethod(MixInClosurePropertiesAsMethodsDynamicObject.java:38)
at org.gradle.groovy.scripts.BasicScript$ScriptDynamicObject.tryInvokeMethod(BasicScript.java:138)
at org.gradle.internal.metaobject.AbstractDynamicObject.invokeMethod(AbstractDynamicObject.java:163)
at org.gradle.api.internal.project.DefaultDynamicLookupRoutine.invokeMethod(DefaultDynamicLookupRoutine.java:58)
at org.gradle.groovy.scripts.BasicScript.invokeMethod(BasicScript.java:87)
at extensionbuild_24upet4amd11vb72gcdt4o5ix.run(D:\proj\w1\source\projects\release\custom-logic.gradle:1)
at org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:91)
... 182 more
I wanted to see if exiting immediately would lead to a better outcome, as there would be no stack-trace:
if ( System.getenv('MY_ENV_VAR') == null ) {
print("Environment variable MY_ENV_VAR is not set, you will not be able to download artifact from the repository")
System.exit(1);
}
The output is shorter, the printed message does not appear anywhere. Plus who knows if there are other unintended side-effects.
Daemon will be stopped at the end of the build
The message received from the daemon indicates that the daemon has disappeared.
Build request sent: Build{id=5ca5bc45-4867-4929-9d95-ea1a542bd965, currentDir=D:\project\release}
Attempting to read last messages from the daemon log...
Daemon pid: 29912
log file: C:\Users\df0\.gradle\daemon\8.9\daemon-29912.out.log
----- Last 20 lines from daemon log file - daemon-29912.out.log -----
2025-05-14T11:10:37.567+0200 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Command execution: started DaemonCommandExecution[command = Build{id=5ca5bc45-4867-4929-9d95-ea1a542bd965, currentDir=D:\project\release}, connection = DefaultDaemonConnection: socket connection from /127.0.0.1:35782 to /127.0.0.1:35783] after 0.006750000000000001 minutes of idle
2025-05-14T11:10:37.567+0200 [INFO] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Marking the daemon as busy, address: [59868c89-5dad-4483-98e8-6438beaf9923 port:35782, addresses:[localhost/127.0.0.1]]
2025-05-14T11:10:37.568+0200 [DEBUG] [org.gradle.launcher.daemon.registry.PersistentDaemonRegistry] Marking busy by address: [59868c89-5dad-4483-98e8-6438beaf9923 port:35782, addresses:[localhost/127.0.0.1]]
2025-05-14T11:10:37.570+0200 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire exclusive lock on daemon addresses registry.
2025-05-14T11:10:37.571+0200 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.
2025-05-14T11:10:37.574+0200 [DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry.
2025-05-14T11:10:37.575+0200 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] resetting idle timer
2025-05-14T11:10:37.575+0200 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon is running. Sleeping until state changes.
2025-05-14T11:10:37.577+0200 [INFO] [org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy] Daemon is about to start building Build{id=5ca5bc45-4867-4929-9d95-ea1a542bd965, currentDir=D:\project\release}. Dispatching build started information...
2025-05-14T11:10:37.578+0200 [DEBUG] [org.gradle.launcher.daemon.server.SynchronizedDispatchConnection] thread 50: dispatching org.gradle.launcher.daemon.protocol.BuildStarted@303cd65d
2025-05-14T11:10:37.582+0200 [DEBUG] [org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment] Configuring env variables: [PATH, WINEDLLOVERRIDES, Platform, WindowsSdkBinPath, SESSIONNAME, ALLUSERSPROFILE, VCToolsRedistDir, WindowsSdkVerBinPath, VSCMD_VER, =ExitCode, PROGRAMFILES, USERNAME, PWD, ProgramFiles(x86), FPS_BROWSER_USER_PROFILE_STRING, NcpClntDataPath, VSCMD_ARG_HOST_ARCH, DEFAULT_JVM_OPTS, PATHEXT, DriverData, vsconsoleoutput, HOMEPATH, PROCESSOR_IDENTIFIER, PUBLIC, ExtensionSdkDir, =::, SHLVL, FrameworkVersion32, __DOTNET_ADD_64BIT, OneDriveCommercial, __DOTNET_PREFERRED_BITNESS, WindowsSdkDir, FPS_BROWSER_APP_PROFILE_STRING, FrameworkDir32, JAVA_HOME, __VSCMD_PREINIT_VS170COMNTOOLS, UCRTVersion, OneDrive, __VSCMD_PREINIT_PATH, =C:, APPDATA, JAVA_EXE, DLD_PATH, EFC_11376, _, VCINSTALLDIR, CHECKPOINT_DISABLE, LIB, COMPUTERNAME, PROFILEREAD, VSCMD_ARG_TGT_ARCH, __VSCMD_PREINIT_VCToolsVersion, =D:, VisualStudioVersion, HOMEDRIVE, FrameworkVersion, HTTP_PROXY_inactive, NUMBER_OF_PROCESSORS, WindowsSDKLibVersion, USERDOMAIN_ROAMINGPROFILE, VCToolsVersion, WindowsSDK_ExecutablePath_x64, PROCESSOR_LEVEL, SYSTEMDRIVE, TZ, LESSHISTFILE, PROCESSOR_ARCHITECTURE, WGETRC, PSModulePath, EXTERNAL_INCLUDE, LOGNAME, USERDNSDOMAIN, FrameworkVersion64, INFOPATH, VCToolsInstallDir, APP_HOME, SHELL, PASSWORD_STORE_DIR, OLDPWD, TMPDIR, WINDIR, ProgramData,ProgramW6432, SYSSCREENRC, GNUPGHOME, LIBPATH, MINTTY_SHORTCUT, WindowsSDKVersion, VSCMD_ARG_app_plat, LS_COLORS, WindowsSDK_ExecutablePath_x86, LOCALAPPDATA, USERDOMAIN, LOGONSERVER, NETFXSDKDir, WindowsLibPath, DISPLAY, UniversalCRTSdkDir, CommandPromptType, GTK_BASEPATH, SYSTEMROOT, __DOTNET_ADD_32BIT, VCIDEInstallDir, Framework40Version, OS, COMMONPROGRAMFILES, COMSPEC, VS170COMNTOOLS, FrameworkDir64, PROCESSOR_REVISION, USER, CLASSPATH, CommonProgramW6432, FrameworkDir, EDITOR, PRINTER, TEMP, HOSTNAME, USERPROFILE, TMP, CommonProgramFiles(x86)]
2025-05-14T11:10:37.608+0200 [DEBUG] [org.gradle.launcher.daemon.server.exec.LogToClient] About to start relaying all logs to the client via the connection.
2025-05-14T11:10:37.608+0200 [INFO] [org.gradle.launcher.daemon.server.exec.LogToClient] The client will now receive all logging from the daemon (pid: 29912). The daemon log file: C:\Users\df0\.gradle\daemon\8.9\daemon-29912.out.log
2025-05-14T11:10:37.610+0200 [DEBUG] [org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon] Requesting daemon stop after processing Build{id=5ca5bc45-4867-4929-9d95-ea1a542bd965, currentDir=D:\project\release}
2025-05-14T11:10:37.610+0200 [LIFECYCLE] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Daemon will be stopped at the end of the build
2025-05-14T11:10:37.611+0200 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Stop as soon as idle requested. The daemon is busy
2025-05-14T11:10:37.611+0200 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] daemon stop has been requested. Sleeping until state changes.
2025-05-14T11:10:37.612+0200 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] The daemon has started executing the build.
2025-05-14T11:10:37.612+0200 [DEBUG] [org.gradle.launcher.daemon.server.exec.ExecuteBuild] Executing build with daemon context: DefaultDaemonContext[uid=d0ff3a65-cb1b-4d24-a7c7-66f6f7a185d6,javaHome=D:\jdk-21.0.2,javaVersion=21,daemonRegistryDir=C:\Users\df0\.gradle\daemon,pid=29912,idleTimeout=120000,priority=NORMAL,applyInstrumentationAgent=true,nativeServicesMode=ENABLED,daemonOpts=-XX:+HeapDumpOnOutOfMemoryError,--add-opens=java.base/java.util=ALL-UNNAMED,--add-opens=java.base/java.lang=ALL-UNNAMED,--add-opens=java.base/java.lang.invoke=ALL-UNNAMED,--add-opens=java.prefs/java.util.prefs=ALL-UNNAMED,--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED,--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED,--add-opens=java.base/java.nio.charset=ALL-UNNAMED,--add-opens=java.base/java.net=ALL-UNNAMED,--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED,-Xmx4608M,-Dfile.encoding=UTF-8,-Duser.country=US,-Duser.language=en,-Duser.variant]
Daemon vm is shutting down... The daemon has exited normally or was terminated in response to a user interrupt.
----- End of the daemon log -----
FAILURE: Build failed with an exception.
* What went wrong:
Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)
* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
* Exception is:
org.gradle.launcher.daemon.client.DaemonDisappearedException: Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)
at org.gradle.launcher.daemon.client.DaemonClient.handleDaemonDisappearance(DaemonClient.java:282)
at org.gradle.launcher.daemon.client.DaemonClient.monitorBuild(DaemonClient.java:255)
at org.gradle.launcher.daemon.client.DaemonClient.executeBuild(DaemonClient.java:208)
at org.gradle.launcher.daemon.client.SingleUseDaemonClient.execute(SingleUseDaemonClient.java:63)
at org.gradle.launcher.daemon.client.SingleUseDaemonClient.execute(SingleUseDaemonClient.java:37)
at org.gradle.launcher.cli.RunBuildAction.run(RunBuildAction.java:56)
at org.gradle.internal.Actions$RunnableActionAdapter.execute(Actions.java:167)
at org.gradle.launcher.cli.DefaultCommandLineActionFactory$ParseAndBuildAction.execute(DefaultCommandLineActionFactory.java:333)
at org.gradle.launcher.cli.DefaultCommandLineActionFactory$ParseAndBuildAction.execute(DefaultCommandLineActionFactory.java:297)
at org.gradle.launcher.cli.DebugLoggerWarningAction.execute(DebugLoggerWarningAction.java:74)
at org.gradle.launcher.cli.DebugLoggerWarningAction.execute(DebugLoggerWarningAction.java:30)
at org.gradle.launcher.cli.WelcomeMessageAction.execute(WelcomeMessageAction.java:96)
at org.gradle.launcher.cli.WelcomeMessageAction.execute(WelcomeMessageAction.java:40)
at org.gradle.launcher.cli.NativeServicesInitializingAction.execute(NativeServicesInitializingAction.java:50)
at org.gradle.launcher.cli.NativeServicesInitializingAction.execute(NativeServicesInitializingAction.java:27)
at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:41)
at org.gradle.launcher.cli.ExceptionReportingAction.execute(ExceptionReportingAction.java:26)
at org.gradle.launcher.cli.DefaultCommandLineActionFactory$WithLogging.execute(DefaultCommandLineActionFactory.java:445)
at org.gradle.launcher.Main.doAction(Main.java:35)
at org.gradle.launcher.bootstrap.EntryPoint.run(EntryPoint.java:52)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.gradle.launcher.bootstrap.ProcessBootstrap.runNoExit(ProcessBootstrap.java:60)
at org.gradle.launcher.bootstrap.ProcessBootstrap.run(ProcessBootstrap.java:37)
at org.gradle.launcher.GradleMain.main(GradleMain.java:31)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.gradle.wrapper.BootstrapMainStarter.start(BootstrapMainStarter.java:35)
at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:108)
at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:63)
ninja: build stopped: subcommand failed.
And at least, in most C and C++ projects, the command to reproduce the error is also often printed out, which helps to diagnose and fix the issue without triggering a build, which might take additional time.
Overview of the build process
To manually build a Java-only Android application, one has to do the following:
-
get dependencies and put them in classpath
-
create
R.java
from resources -
compile
.java
files to.class
files -
optionally optimize
.class
files -
compile
.class
files to.dex
files -
package everything together
-
sign the application
Until 2021 one would have used the tool dx
for compiling .class
files to .dex
; the current program to use should be d8
.
Similarly, proguard
is not recommended anymore for optimizing .class
files, one should use r8
. Although, since proguard
is still mantained, I guess it is fine to continue to use it.
Last but not least, if you want to publish your application on the Google Play Store, you have to submit an Android Application Bundle (.aab
) and not an Android Package.
Since an .apk
can be created from an .aab
, I suppose that if you also need an .aab
, that it is better to only create an aab
, and then an apk
from it; at least to ensure consistency.
Note 📝 | I failed to find a good resource online that covers building a program by hand. Most resources are outdated, do not mention proguard /r8 , do not handle aab and aar , or projects with multiple languages. |
Scope of this notes
I want to summarize how to create a "hello world" Android Application (as apk
and aab
) with Java, Kotlin, some native source code, and a couple of resources with a minimal environment.
In particular I wanted to cover following topics:
-
packaging
.apk
-
packaging
.aab
-
create an
.apk
from an.aab
-
signing
-
use string and dialog as resources
-
a java function calling a kotlin function
-
a kotlin function calling a java function
-
a java function calling a C++ function
-
a C++ function calling a java function
Topics that will be missing are
-
resolving and downloading external dependencies
-
creating an
.aar
Setup
Thankfully Debian already packaged most tools for building a program for Android. This makes it easier to create a reproducible environment.
In the following table, I’ve enlisted some packages and tools that I’ve tested and are provided by the package manager:
Package | Tool |
---|---|
google-android-build-tools-36.0.0-installer |
|
google-android-platform-36-installer |
|
google-android-ndk-r28-installer |
|
apksigner |
|
zipalign |
|
aapt |
|
dalvik-exchange |
|
adb |
|
kotlin |
|
openjdk-21-jdk-headless |
|
proguard-cli |
|
signapk |
|
There is apparently no package that provides d8
directly, similarly to how dalvik-exchange
provides dx
directly, but it is part of google-android-build-tools-36.0.0-installer
, which conflicts with zipalign
, apksigner
, and aapt
, as it already provides those tools.
signapk
is an alternative to apksigner
. According to the documentation it has a compatible interface with apksigner
, so it is possible to replace one program with another.
Since dx
might not work well with never Java feature and development kits, one should generally prefer d8
over dx
.
If you want to create an aab
, you will need bundletool 🗄️.
Unfortunately there does not seem to exist a corresponding Debian package. At least it is a single .jar
file that can be downloaded from GitHub without additional dependencies.
If you are not planning to use Kotlin, then obviously you do not need to install kotlinc
, just like installing the ndk
is not required if there is no "native code".
There are some additional packages that look interesting; for example android-sdk
. According to the Debian Wiki 🗄️, this package is deprecated. There are also android-platform-tools-base
, android-sdk-build-tools
, and android-sdk-platform-tools
; I did not find any information about them, but they are not necessary for the project I made, so I did not investigate further.
Note 📝 | some packages might be missing from the current Debian distribution. You can either add packages from other Debian versions, or download the missing packages (and dependencies) from other version and install the manually. In my case, dalvik-exchange was missing from Debian testing and I installed the version from unstable. |
Reduce kotlin
dependencies
The kotlin
package has following dependencies, that are not used by the kotlin compiler kotlinc
: ant
, libmaven-compiler-plugin-java
, libmaven-plugin-tools-java
, and libmaven3-core-java
.
Those are not required, since I do not plan to use Ant, Maven, or Gradle, and since kotlinc
does not depend on them.
With equivs
one can install dummy packages to break to dependencies, which means one can avoid to install over 300MB of unused data (depending if you do not have any of the dependencies of those pakages already installed). This makes only sense if you know you will not need packages that rely on ant
, libmaven-compiler-plugin-java
, libmaven-plugin-tools-java
, or libmaven3-core-java
. Otherwise, one could grab the Debian kotlin
package and modify it’s metadata by removing the unused dependencies.
Compile a sample C++ program
First thing I did was trying to compile a C++ program, to see if the toolchain file works. Given the source file main.cpp
and the project file CMakeLists.txt
#include <cstdio>
int main(){
std::puts("Hello World!");
}
cmake_minimum_required(VERSION 3.30)
project(AndroidApp)
message("C compiler ${CMAKE_C_COMPILER}")
message("C++ compiler ${CMAKE_CXX_COMPILER}")
message("ANDROID_NDK ${ANDROID_NDK}")
message("ANDROID_SDK_ROOT ${ANDROID_SDK_ROOT}")
message("CMAKE_SYSTEM_VERSION ${CMAKE_SYSTEM_VERSION}")
message("CMAKE_ANDROID_API ${CMAKE_ANDROID_API}")
message("ANDROID_PLATFORM ${ANDROID_PLATFORM}")
add_executable(main main.cpp)
One can compile the source file with following commands
cmake -DANDROID_ABI=x86_64 --toolchain=/usr/lib/android-ndk/build/cmake/android.toolchain.cmake -S. -B/tmp/build && cmake --build /tmp/build && file /tmp/build/main
/tmp/build/main: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, BuildID[sha1]=e07b2e2ac2eae4393938398c3940fbd85c5b930e, with debug_info, not stripped
Create a key for signing the artifacts
Packages needs to be signed; for development, just create a dummy key pair with the following command:
keytool -genkeypair -keystore keystore.jks -alias androidkey -validity 10000 -keyalg RSA -keysize 2048 -storepass android -keypass android
First attempt
After some trial and errors, this is how a barebone CMake file for packaging a minimal Android application could looks like:
cmake_minimum_required(VERSION 3.31)
project(AndroidApp)
set(CLASSPATH_SEPARATOR ":")
if(CMAKE_HOST_WIN32)
set(CLASSPATH_SEPARATOR ";") # unfortunately not possible to use ; on all platforms
endif()
if(NOT ANDROID_SDK_ROOT)
if($ENV{ANDROID_SDK_ROOT})
set(ANDROID_SDK_ROOT $ENV{ANDROID_SDK_ROOT} CACHE PATH "Path to Android SDK")
else()
set(ANDROID_SDK_ROOT "/usr/lib/android-sdk/" CACHE PATH "Path to Android SDK")
endif()
endif()
if(NOT BUILD_TOOLS)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/build-tools "${ANDROID_SDK_ROOT}/build-tools/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING)
set(BUILD_TOOLS)
foreach(child ${children})
if(IS_DIRECTORY ${ANDROID_SDK_ROOT}/build-tools/${child})
list(APPEND BUILD_TOOLS ${ANDROID_SDK_ROOT}/build-tools/${child})
endif()
endforeach()
endif()
find_program(AAPT NAMES aapt REQUIRED PATHS ${BUILD_TOOLS})
find_program(APKSIGNER NAMES apksigner signapk REQUIRED PATHS ${BUILD_TOOLS})
find_program(ZIPALIGN NAMES zipalign REQUIRED PATHS ${BUILD_TOOLS})
execute_process(COMMAND "${ZIPALIGN}" OUTPUT_VARIABLE ZIPALIGN_OUTPUT ERROR_VARIABLE ZIPALIGN_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
string(FIND "${ZIPALIGN_OUTPUT}" " -P " position)
set(ZIPALIGN_ALIGNEMNT_ARG "-p")
if(position GREATER -1)
set(ZIPALIGN_ALIGNEMNT_ARG "-P" "16") # from android 14(?), different page sizes are supported
endif()
find_program(DX NAMES dx REQUIRED PATHS ${BUILD_TOOLS})
find_program(KOTLINC NAMES kotlinc REQUIRED)
set(KOTLIN_STDLIB "/usr/share/java/kotlin-stdlib.jar")
find_package(Java 1.8)
find_package(Java COMPONENTS Development)
include(UseJava) # java, jar, and other tools
if(NOT ANDROID_PLATFORM_DIR)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/platforms "${ANDROID_SDK_ROOT}/platforms/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING) # NOTE: also considers ${ANDROID_SDK_ROOT}/build-tools/debian
list(GET children 0 ANDROID_PLATFORM_DIR)
if(ANDROID_PLATFORM_DIR STRLESS ANDROID_PLATFORM)
message(FATAL_ERROR "ERROR: no valid platform found. Found ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR}, but it is less than configured minimum supported version: ${ANDROID_PLATFORM}")
endif()
set(ANDROID_PLATFORM_DIR ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR})
endif()
find_file(ANDROID_JAR
NAMES android.jar
PATHS ${ANDROID_PLATFORM_DIR}
REQUIRED NO_CMAKE_FIND_ROOT_PATH
)
set(BASE_CLASSPATH_FOR_JAVAC "${ANDROID_JAR}")
if(KOTLIN_STDLIB)
# required at least for class kotlin.Metadata
set(BASE_CLASSPATH_FOR_JAVAC "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${KOTLIN_STDLIB}")
endif()
add_library(hello SHARED
src/hello.cpp
)
target_link_libraries(hello
PRIVATE
log # for logging with __android_log_write
)
set(CLASS_APP_FOLDER "${CMAKE_BINARY_DIR}/class_app_apk")
# PROJECT files
set(INPUTFILES_JAVA
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/MainActivity.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/Hello.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/HelloFromCPP.java
)
set(INPUTFILES_KOTLIN ${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/kotlin.kt)
set(STRINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/values/strings.xml")
set(IDS "${CMAKE_CURRENT_SOURCE_DIR}/res/values/ids.xml")
set(LAYOUTS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/layout/activity_main.xml")
set(MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/src/AndroidManifest.xml")
set(FINAL_APK "${CMAKE_CURRENT_BINARY_DIR}/app.apk")
# FIXME: should enlist all resources
set(RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml")
add_custom_command(
OUTPUT ${RESOURCES}
DEPENDS ${LAYOUTS_XML} ${STRINGS_XML}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/drawable/
COMMAND ${CMAKE_COMMAND} -E copy ${LAYOUTS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml
COMMAND ${CMAKE_COMMAND} -E copy ${STRINGS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/strings.xml
COMMAND ${CMAKE_COMMAND} -E copy ${IDS} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/ids.xml
COMMENT "copy relevant resources to build folder"
VERBATIM
)
set(CLASSPATH_GEN_APP ${CMAKE_CURRENT_BINARY_DIR}/gen_app_apk/)
set(R_JAVA ${CLASSPATH_GEN_APP}/com/example/R.java)
add_custom_command(
OUTPUT ${R_JAVA}
DEPENDS ${MANIFEST} ${STRINGS_XML} ${LAYOUTS_XML} ${RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASSPATH_GEN_APP}
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROGUARDRULES_D_APP}
COMMAND ${AAPT} package -f -m -J ${CLASSPATH_GEN_APP} -S ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk -M ${MANIFEST} -I ${ANDROID_JAR}
COMMENT "create R.java file with aapt"
VERBATIM
CODEGEN
)
set(CLASS_FILES_KOTLIN)
foreach(file IN LISTS INPUTFILES_KOTLIN)
string(REGEX REPLACE "[.]kt$" ".class" classfile ${file})
set(classfile "${CLASS_APP_FOLDER}/kotlin/${classfile}")
list(APPEND CLASS_FILES_KOTLIN ${classfile})
endforeach()
add_custom_command(
OUTPUT ${CLASS_FILES_KOTLIN}
DEPENDS ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA}
COMMAND ${KOTLINC} -jvm-target 1.8 -classpath "${ANDROID_JAR}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_APP}" -d ${CLASS_APP_FOLDER}/kotlin ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA}
COMMENT "kotlinc"
VERBATIM
)
set(CLASS_FILES_JAVA)
foreach(file IN LISTS INPUTFILES_JAVA)
string(REGEX REPLACE "[.]java$" ".class" classfile ${file})
set(classfile "${CLASS_APP_FOLDER}/java/${classfile}")
list(APPEND CLASS_FILES_JAVA ${classfile})
endforeach()
add_custom_command(
OUTPUT ${CLASS_FILES_JAVA}
DEPENDS ${INPUTFILES_JAVA} ${R_JAVA} ${CLASS_FILES_KOTLIN}
COMMAND ${Java_JAVAC_EXECUTABLE} -Xlint:all --class-path "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${CLASS_APP_FOLDER}/kotlin${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_APP}" --source 8 --target 8 -d ${CLASS_APP_FOLDER}/java ${INPUTFILES_JAVA};
COMMENT "javac"
VERBATIM
)
set(CLASS_FILES ${CLASS_FILES_KOTLIN} ${CLASS_FILES_JAVA})
set(DIR_CLASS_FILES ${CLASS_APP_FOLDER}/java ${CLASS_APP_FOLDER}/kotlin)
set(DEX_FILE ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/classes.dex)
add_custom_command(
OUTPUT ${DEX_FILE}
DEPENDS ${MANIFEST} ${CLASS_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk
COMMAND ${DX} --dex --min-sdk-version=23 --output=${DEX_FILE} ${DIR_CLASS_FILES}
COMMENT "convert .class to .dex file (dx)"
VERBATIM
)
set(UNSIGNED_APK "${CMAKE_CURRENT_BINARY_DIR}/app.unsigned.apk")
add_custom_command(
OUTPUT ${UNSIGNED_APK}
DEPENDS ${MANIFEST} ${DEX_FILE} hello ${RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/lib/${ANDROID_ABI}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libhello.so ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/lib/${ANDROID_ABI}
COMMAND ${AAPT} package -f -F ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk -I ${ANDROID_JAR} -M ${MANIFEST} -S ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk base_app_apk
COMMAND ${ZIPALIGN} ${ZIPALIGN_ALIGNEMNT_ARG} -f 4 ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk ${UNSIGNED_APK}
COMMAND ${CMAKE_COMMAND} -E remove ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk
COMMENT "create unsigned apk"
VERBATIM
)
set(KEYSTORE "${CMAKE_CURRENT_SOURCE_DIR}/keystore.jks")
add_custom_command(
OUTPUT ${FINAL_APK}
DEPENDS ${UNSIGNED_APK}
COMMAND ${ZIPALIGN} ${ZIPALIGN_ALIGNEMNT_ARG} -f 4 ${UNSIGNED_APK} ${FINAL_APK}
COMMAND ${APKSIGNER} sign --in ${FINAL_APK} -ks ${KEYSTORE} --ks-key-alias androidkey --ks-pass pass:android --key-pass pass:android --min-sdk-version 21 --v2-signing-enabled true --v1-signing-enabled false
COMMENT "create final signed apk"
VERBATIM
)
add_custom_target(apk ALL DEPENDS ${FINAL_APK})
Some things to note.
The first thing is that in a project with both Java and Kotlin source files, one needs to handle the dependencies between both languages. Java files depend on Java and class files, thus the first thing to do is to compile all, if any, Kotlin source files, to .class
files. The compiled .class
files can be added to the CLASSPATH for the Java compiler.
Easy-peasy.
Except that Kotlin code can depend on Java code, creating a circular dependency between the languages, and even between source files.
The kotlinc
compiler accepts Java files as dependencies 🗄️ for Kotlin files. This makes it possible to make a more strict division between the two languages, without having to throw everything together.
Native code and code that compiles to .class
files does not interact, except when you package everything together.
The second thing is that the output of javac
is hard to predict, as it depends on the content of the java
files. Thus, tracking which files have been compiled and are out-of-date is generally broken.
Third: apparently a lot of tools like to work with directories instead of files; for example aapt
. Since I do not want "temporary files in the source directory" (for example a file I just copied over) to be automatically included in the project, the only solution I’ve found is to copy the relevant files in the build folder. Thus the custom command for copying resources in the build folder.
Fourth, if javac
is new enough, the tool dx
will complain if you do not use --target 8
. As I found out later, dx
has been deprecated since 2021 🗄️, and replaced by d8
.
Fifth, at least one path is completely hard-coded (/usr/share/java/kotlin-stdlib.jar
) and for other paths the hints given to find_program
are hard-coded too. Assuming that most people install the tools through Android Studio and not the package manager, the CMake file should provide a way to define additional paths where to search for libraries and tools.
Last but not least, the CMake file is by design very low-level and error-prone. There are no targets, and no functions to encapsulate common logic. I wanted to provide a simple example on how to create and .apk
. You should not consider this project, even for most toy projects. This also holds for the rest of the presented CMake files in this notes.
Interaction between class files and native code
It is not strictly true that native code and code that compiles to .class
files do not interact.
javac
can create header files with the optional parameter -h
, and before javac
had that functionality, there was a separate tool named javah
that worked on .class
and .java
files, but has been officially removed since JDK 10 🗄️.
Since similarly to other tools, the content of the input files determines the name of the output file, it is not that easy to explicitely track what files are out of date.
Since javah
is not supported anymore, I assumed that a similar functionality exists also for Kotlin, but it turns out there is an open issue since 2019 🗄️.
Instead of generating header files, and thus making the native code depend on the Java code, one could verify after building both the library and class files if all functions signature in the class files can be found in the libraries. The advantage of this method is that it removes a build-time dependency between different languages, making it easier to parallelize the build, and does not require to parse multiple languages for the JVM (Java, Kotlin, Scala, …) and multiple languages that are acompile to native code (C, C++, zig, …) but only requires to parse .class
and .so
files.
The main advantage of this approach, is that it might help to find exports that are missing, or exports that are not necessary anymore.
The main disadvantage is that it is not able to verify if the function signature match. To verify if the function signatures match, one needs the functions declarations from the native source code, which means parsing the source code.
With the header files, the compiler itself will determine, if the header file has been included at the right location and the code is written in a certain way, if the signature is correct.
Improvements
After some thought, I decided that working with jar
files instead of .class
file makes more sense. In a C++ project, it is also easier to reason about libraries instead of single object files.
Most important, it solves the issue that it is hard to predict the output of javac
.
A second improvement is the integration of proguard
, an optimizer for .class
files, for which it is also difficult to predict what files it will create. I would have tried to integrate r8
too, but it does not seem to be packaged anywhere, and I did not find out (yet) how to download it cleanly.
A third improvement is the usage of d8
instead of dx
.
cmake_minimum_required(VERSION 3.31)
project(AndroidApp)
set(CLASSPATH_SEPARATOR ":")
if(CMAKE_HOST_WIN32)
set(CLASSPATH_SEPARATOR ";") # unfortunately not possible to use ; on all platforms
endif()
if(NOT ANDROID_SDK_ROOT)
if($ENV{ANDROID_SDK_ROOT})
set(ANDROID_SDK_ROOT $ENV{ANDROID_SDK_ROOT} CACHE PATH "Path to Android SDK")
else()
set(ANDROID_SDK_ROOT "/usr/lib/android-sdk/" CACHE PATH "Path to Android SDK")
endif()
endif()
if(NOT BUILD_TOOLS)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/build-tools "${ANDROID_SDK_ROOT}/build-tools/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING)
set(BUILD_TOOLS)
foreach(child ${children})
if(IS_DIRECTORY ${ANDROID_SDK_ROOT}/build-tools/${child})
list(APPEND BUILD_TOOLS ${ANDROID_SDK_ROOT}/build-tools/${child})
endif()
endforeach()
endif()
find_program(AAPT NAMES aapt REQUIRED PATHS ${BUILD_TOOLS})
find_program(AAPT2 NAMES aapt2 REQUIRED PATHS ${BUILD_TOOLS})
find_program(APKSIGNER NAMES apksigner signapk REQUIRED PATHS ${BUILD_TOOLS})
find_program(ZIPALIGN NAMES zipalign REQUIRED PATHS ${BUILD_TOOLS})
execute_process(COMMAND "${ZIPALIGN}" OUTPUT_VARIABLE ZIPALIGN_OUTPUT ERROR_VARIABLE ZIPALIGN_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
string(FIND "${ZIPALIGN_OUTPUT}" " -P " position)
set(ZIPALIGN_ALIGNEMNT_ARG "-p")
if(position GREATER -1)
set(ZIPALIGN_ALIGNEMNT_ARG "-P" "16") # from android 14(?), different page sizes are supported
endif()
find_program(DX NAMES dx PATHS ${BUILD_TOOLS})
find_program(D8 NAMES d8 PATHS ${BUILD_TOOLS})
if(NOT DX AND NOT D8)
message(FATAL_ERROR "At least dx or d8 is required, none found in ${BUILD_TOOLS}")
endif()
find_program(KOTLINC NAMES kotlinc REQUIRED)
set(KOTLIN_STDLIB "/usr/share/java/kotlin-stdlib.jar")
find_program(PROGUARD NAMES proguard)
find_package(Java 1.8)
find_package(Java COMPONENTS Development)
include(UseJava) # java, jar, and other tools
set(BUNDLETOOL ${Java_JAVA_EXECUTABLE} -jar ${CMAKE_SOURCE_DIR}/bundletool-all-1.18.1.jar)
if(NOT ANDROID_PLATFORM_DIR)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/platforms "${ANDROID_SDK_ROOT}/platforms/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING)
list(GET children 0 ANDROID_PLATFORM_DIR)
if(ANDROID_PLATFORM_DIR STRLESS ANDROID_PLATFORM)
message(FATAL_ERROR "ERROR: no valid platform found. Found ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR}, but it is less than configured minimum supported version: ${ANDROID_PLATFORM}")
endif()
set(ANDROID_PLATFORM_DIR ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR})
endif()
find_file(ANDROID_JAR
NAMES android.jar
PATHS ${ANDROID_PLATFORM_DIR}
REQUIRED NO_CMAKE_FIND_ROOT_PATH
)
add_executable(main src/main.cpp)
add_library(hello SHARED
src/hello.cpp
)
target_link_libraries(hello
PRIVATE
log # required for logging with __android_log_write
)
set(CLASS_APP_FOLDER "${CMAKE_BINARY_DIR}/class_app_apk")
# PROJECT files
set(INPUTFILES_JAVA
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/MainActivity.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/Hello.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/HelloFromCPP.java
)
set(INPUTFILES_KOTLIN ${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/kotlin.kt)
set(BASE_CLASSPATH_FOR_JAVAC "${ANDROID_JAR}")
if(KOTLIN_STDLIB)
# required at least for class kotlin.Metadata
set(BASE_CLASSPATH_FOR_JAVAC "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${KOTLIN_STDLIB}")
endif()
set(STRINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/values/strings.xml")
set(IDS "${CMAKE_CURRENT_SOURCE_DIR}/res/values/ids.xml")
set(LAYOUTS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/layout/activity_main.xml")
set(MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/src/AndroidManifest.xml")
# create APK directly
set(FINAL_APK "${CMAKE_CURRENT_BINARY_DIR}/app.apk")
# FIXME: iterate like java files
set(RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml")
add_custom_command(
OUTPUT ${RESOURCES}
DEPENDS ${LAYOUTS_XML} ${STRINGS_XML}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/drawable/
COMMAND ${CMAKE_COMMAND} -E copy ${LAYOUTS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml
COMMAND ${CMAKE_COMMAND} -E copy ${STRINGS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/strings.xml
COMMAND ${CMAKE_COMMAND} -E copy ${IDS} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/ids.xml
COMMENT "copy relevant resources to build folder"
VERBATIM
)
set(CLASSPATH_GEN_APP ${CMAKE_CURRENT_BINARY_DIR}/gen_app_apk/)
set(PROGUARDRULES_D_APP ${CMAKE_CURRENT_BINARY_DIR}/proguard_rules/)
set(PROGUARDRULES_F_APP ${PROGUARDRULES_D_APP}/aapt.pro)
set(R_JAVA ${CLASSPATH_GEN_APP}/com/example/R.java)
add_custom_command(
OUTPUT ${R_JAVA} ${PROGUARDRULES_F_APP}
DEPENDS ${MANIFEST} ${STRINGS_XML} ${LAYOUTS_XML} ${RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASSPATH_GEN_APP}
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROGUARDRULES_D_APP}
COMMAND ${AAPT} package -f -m -J ${CLASSPATH_GEN_APP} -S ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk -G ${PROGUARDRULES_F_APP} -M ${MANIFEST} -I ${ANDROID_JAR}
COMMENT "create R.java file with aapt"
VERBATIM
CODEGEN
)
set(CLASS_FILES_KOTLIN ${CLASS_APP_FOLDER}/kotlin.jar)
add_custom_command(
OUTPUT ${CLASS_FILES_KOTLIN}
DEPENDS ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASS_APP_FOLDER} # not required when not creating jar file
COMMAND ${KOTLINC} -jvm-target 1.8 -classpath "${ANDROID_JAR}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_APP}" -d ${CLASS_FILES_KOTLIN} ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA};
COMMENT "kotlinc"
VERBATIM
)
set(CLASS_FILES_JAVA ${CLASS_APP_FOLDER}/java.jar)
add_custom_command(
OUTPUT ${CLASS_FILES_JAVA}
DEPENDS ${INPUTFILES_JAVA} ${R_JAVA} ${CLASS_FILES_KOTLIN}
COMMAND ${Java_JAVAC_EXECUTABLE} -Xlint:all --class-path "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_APP}" --source 8 --target 8 -d ${CLASS_APP_FOLDER}/java ${INPUTFILES_JAVA};
COMMAND ${Java_JAR_EXECUTABLE} --date "1980-01-01T12:01:00Z" --create --no-manifest --file ${CLASS_FILES_JAVA} -C ${CLASS_APP_FOLDER}/java .;
COMMENT "javac + jar"
VERBATIM
)
if(PROGUARD)
set(CLASSPATH_FOR_PROGUARD ${ANDROID_JAR})
if(CLASS_FILES_KOTLIN)
# required at least for class kotlin.Metadata
set(CLASSPATH_FOR_PROGUARD "${CLASSPATH_FOR_PROGUARD}${CLASSPATH_SEPARATOR}/usr/share/java/kotlin-stdlib.jar")
endif()
set(OPTIMIZED_CLASS ${CLASS_APP_FOLDER}/optimized.jar)
add_custom_command(
OUTPUT ${OPTIMIZED_CLASS}
DEPENDS ${CLASS_FILES_KOTLIN} ${CLASS_FILES_JAVA}
COMMAND ${PROGUARD} -dontobfuscate -libraryjars ${CLASSPATH_FOR_PROGUARD}
-injars ${CLASS_FILES_JAVA}${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN}
-outjars ${OPTIMIZED_CLASS}
-dontwarn org.jetbrains.annotations.NotNull # used only for static analysis, safe to ignore, no need to add other jars to classpath
# should be user-supplied; callback for jni
-keep "public class com.example.hello.HelloFromCPP{public static void log();}"
-include ${PROGUARDRULES_F_APP} # references com.example.MainActivity
COMMENT "optimize .class files"
VERBATIM
)
set(CLASS_FILES ${OPTIMIZED_CLASS})
else()
set(CLASS_FILES ${CLASS_FILES_KOTLIN} ${CLASS_FILES_JAVA})
endif()
set(DEX_FILE ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/classes.dex)
if(D8) # prefer d8 to dx if available
add_custom_command(
OUTPUT ${DEX_FILE}
DEPENDS ${MANIFEST} ${CLASS_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk
COMMAND ${D8} --output ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/ --classpath ${ANDROID_JAR} ${CLASS_FILES}
COMMENT "convert .class to .dex file (d8)"
VERBATIM
)
else()
add_custom_command(
OUTPUT ${DEX_FILE}
DEPENDS ${MANIFEST} ${CLASS_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk
COMMAND ${DX} --dex --min-sdk-version=23 --output=${DEX_FILE} ${CLASS_FILES}
COMMENT "convert .class to .dex file (dx)"
VERBATIM
)
endif()
set(UNSIGNED_APK "${CMAKE_CURRENT_BINARY_DIR}/app.unsigned.apk")
add_custom_command(
OUTPUT ${UNSIGNED_APK}
DEPENDS ${MANIFEST} ${DEX_FILE} hello ${RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/lib/${ANDROID_ABI}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libhello.so ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/lib/${ANDROID_ABI}
COMMAND ${AAPT} package -f -F ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk -I ${ANDROID_JAR} -M ${MANIFEST} -S ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk base_app_apk
COMMAND ${ZIPALIGN} ${ZIPALIGN_ALIGNEMNT_ARG} -f 4 ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk ${UNSIGNED_APK}
COMMAND ${CMAKE_COMMAND} -E remove ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk
COMMENT "create unsigned apk"
VERBATIM
)
set(KEYSTORE "${CMAKE_CURRENT_SOURCE_DIR}/keystore.jks")
add_custom_command(
OUTPUT ${FINAL_APK}
DEPENDS ${UNSIGNED_APK}
COMMAND ${ZIPALIGN} ${ZIPALIGN_ALIGNEMNT_ARG} -f 4 ${UNSIGNED_APK} ${FINAL_APK}
COMMAND ${APKSIGNER} sign --in ${FINAL_APK} -ks ${KEYSTORE} --ks-key-alias androidkey --ks-pass pass:android --key-pass pass:android --min-sdk-version 21 --v2-signing-enabled true --v1-signing-enabled false
COMMENT "create final signed apk"
VERBATIM
)
add_custom_target(apk ALL DEPENDS ${FINAL_APK})
After the first implementation, I immediately verified if kotlinc
and proguard
generated the same jar
archive at every run. Unfortunately not; at least the timestamp differs.
There is even an article about the importance of Reproducible Kotlin Compiler Artifacts 🗄️, and only at this point I realize that the version of kotlin
packaged for Debian is from 2018 (Kotlin 1.3). The current version of the kotlin compiler (currently version is 2.1.21) seems to create by default a reproducible archive.
Also proguard
does not seem to support reproducible builds 🗄️
At least aapt
creates by default a reproducible apk
file.
Add support for bundle files
For creating an android bundle, one should use aapt2
instead of aapt
, and bundletool
.
bundletool
is currently not packaged for Debian, at least it is easy to add it to your system; just download the .jar
from GitHub, and execute it with java -jar
. No additional files are required.
Note 📝 | according to the documentation 🗄️, one should download aapt2 separately for creating a working aab . As far as I could see and test, this is not the case. Maybe the documentation is out-of-date. |
An important thing to realize is that the R.java
file created by aapt2
is different from R.java
created by aapt
.
A R.java
file mostly contains integral constants, for example:
public final class R {
public static final class id {
public static final int my_text=0x7f010000;
}
public static final class layout {
public static final int activity_main=0x7f020000;
}
public static final class string {
public static final int app_name=0x7f030000;
public static final int made_with_cmake=0x7f030001;
}
}
Since one of the few optimisations is constant propagation for native types, one should not reuse class files that depend on R.java
.
What happens in practice is that when the application is executed, the linked resource will not be found, or the wrong will be selected. I’ve spent too many hours debugging this issue, since at the beginning I did not realize that aapt2
also had an option for creating R.java
.
cmake_minimum_required(VERSION 3.31)
project(AndroidApp)
set(CLASSPATH_SEPARATOR ":")
if(CMAKE_HOST_WIN32)
set(CLASSPATH_SEPARATOR ";") # unfortunately not possible to use ; on all platforms
endif()
if(NOT ANDROID_SDK_ROOT)
if($ENV{ANDROID_SDK_ROOT})
set(ANDROID_SDK_ROOT $ENV{ANDROID_SDK_ROOT} CACHE PATH "Path to Android SDK")
else()
set(ANDROID_SDK_ROOT "/usr/lib/android-sdk/" CACHE PATH "Path to Android SDK")
endif()
endif()
if(NOT BUILD_TOOLS)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/build-tools "${ANDROID_SDK_ROOT}/build-tools/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING)
set(BUILD_TOOLS)
foreach(child ${children})
if(IS_DIRECTORY ${ANDROID_SDK_ROOT}/build-tools/${child})
list(APPEND BUILD_TOOLS ${ANDROID_SDK_ROOT}/build-tools/${child})
endif()
endforeach()
endif()
find_program(AAPT2 NAMES aapt2 REQUIRED PATHS ${BUILD_TOOLS})
find_program(APKSIGNER NAMES apksigner signapk REQUIRED PATHS ${BUILD_TOOLS})
find_program(ZIPALIGN NAMES zipalign REQUIRED PATHS ${BUILD_TOOLS})
execute_process(COMMAND "${ZIPALIGN}" OUTPUT_VARIABLE ZIPALIGN_OUTPUT ERROR_VARIABLE ZIPALIGN_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
string(FIND "${ZIPALIGN_OUTPUT}" " -P " position)
set(ZIPALIGN_ALIGNEMNT_ARG "-p")
if(position GREATER -1)
set(ZIPALIGN_ALIGNEMNT_ARG "-P" "16") # from android 14(?), different page sizes are supported
endif()
find_program(DX NAMES dx PATHS ${BUILD_TOOLS})
find_program(D8 NAMES d8 PATHS ${BUILD_TOOLS})
if(NOT DX AND NOT D8)
message(FATAL_ERROR "At least dx or d8 is required, none found in ${BUILD_TOOLS}")
endif()
find_program(KOTLINC NAMES kotlinc REQUIRED)
set(KOTLIN_STDLIB "/usr/share/java/kotlin-stdlib.jar")
find_program(PROGUARD NAMES proguard)
find_package(Java 1.8)
find_package(Java COMPONENTS Development)
include(UseJava) # java, jar, and other tools
set(BUNDLETOOL ${Java_JAVA_EXECUTABLE} -jar ${CMAKE_SOURCE_DIR}/bundletool-all-1.18.1.jar)
if(NOT ANDROID_PLATFORM_DIR)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/platforms "${ANDROID_SDK_ROOT}/platforms/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING)
list(GET children 0 ANDROID_PLATFORM_DIR)
if(ANDROID_PLATFORM_DIR STRLESS ANDROID_PLATFORM)
message(FATAL_ERROR "ERROR: no valid platform found. Found ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR}, but it is less than configured minimum supported version: ${ANDROID_PLATFORM}")
endif()
set(ANDROID_PLATFORM_DIR ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR})
endif()
find_file(ANDROID_JAR
NAMES android.jar
PATHS ${ANDROID_PLATFORM_DIR}
REQUIRED NO_CMAKE_FIND_ROOT_PATH
)
add_executable(main src/main.cpp)
add_library(hello SHARED
src/hello.cpp
)
target_link_libraries(hello
PRIVATE
log # required for logging with __android_log_write
)
# PROJECT files
set(INPUTFILES_JAVA
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/MainActivity.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/Hello.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/HelloFromCPP.java
)
set(INPUTFILES_KOTLIN ${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/kotlin.kt)
set(BASE_CLASSPATH_FOR_JAVAC "${ANDROID_JAR}")
if(KOTLIN_STDLIB)
# required at least for class kotlin.Metadata
set(BASE_CLASSPATH_FOR_JAVAC "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${KOTLIN_STDLIB}")
endif()
set(STRINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/values/strings.xml")
set(IDS "${CMAKE_CURRENT_SOURCE_DIR}/res/values/ids.xml")
set(LAYOUTS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/layout/activity_main.xml")
set(MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/src/AndroidManifest.xml")
# FIXME: iterate like java files
set(RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml")
add_custom_command(
OUTPUT ${RESOURCES}
DEPENDS ${LAYOUTS_XML} ${STRINGS_XML}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/drawable/
COMMAND ${CMAKE_COMMAND} -E copy ${LAYOUTS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml
COMMAND ${CMAKE_COMMAND} -E copy ${STRINGS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/strings.xml
COMMAND ${CMAKE_COMMAND} -E copy ${IDS} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/ids.xml
COMMENT "copy relevant resources to build folder"
VERBATIM
)
set(FLAT_RESOURCES ${CMAKE_BINARY_DIR}/flat_res/layout_activity_main.xml.flat ${CMAKE_BINARY_DIR}/flat_res/values_strings.arsc.flat ${CMAKE_BINARY_DIR}/flat_res/values_ids.arsc.flat)
add_custom_command(
OUTPUT ${FLAT_RESOURCES}
DEPENDS ${STRINGS_XML} ${IDS} ${LAYOUTS_XML}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/flat_res # otherwise aapt2 creates zip archive
COMMAND ${AAPT2} compile -o ${CMAKE_BINARY_DIR}/flat_res/ ${STRINGS_XML} ${IDS} ${LAYOUTS_XML}
COMMENT "compile resources to flat"
VERBATIM
)
set(CLASSPATH_GEN_BUNDLE ${CMAKE_CURRENT_BINARY_DIR}/gen_app_bundle/)
set(R_JAVA_BUNDLE ${CLASSPATH_GEN_BUNDLE}/com/example/R.java)
set(PROGUARDRULES_D_BUNDLE ${CMAKE_CURRENT_BINARY_DIR}/proguard_rules_bundle/)
set(PROGUARDRULES_F_BUNDLE ${PROGUARDRULES_D_BUNDLE}/aapt.pro)
add_custom_command(
OUTPUT ${R_JAVA_BUNDLE} ${PROGUARDRULES_F_BUNDLE}
DEPENDS ${FLAT_RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/output_ # otherwise aapt2 link errors
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASSPATH_GEN_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROGUARDRULES_D_BUNDLE}
COMMAND ${AAPT2} link --proto-format -o ${CMAKE_BINARY_DIR}/output_
-I ${ANDROID_JAR}
--manifest ${MANIFEST}
-R ${FLAT_RESOURCES}
--output-to-dir
--java ${CLASSPATH_GEN_BUNDLE}/
--proguard ${PROGUARDRULES_F_BUNDLE}
COMMENT "aapt2 link and create R.java"
VERBATIM
CODEGEN
)
set(CLASS_BUNDLE_FOLDER "${CMAKE_BINARY_DIR}/class_app_bundle")
set(CLASS_FILES_KOTLIN_BUNDLE "${CLASS_BUNDLE_FOLDER}/kotlin.jar")
add_custom_command(
OUTPUT ${CLASS_FILES_KOTLIN_BUNDLE}
DEPENDS ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASS_BUNDLE_FOLDER}
COMMAND ${KOTLINC} -jvm-target 1.8 -classpath "${ANDROID_JAR}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_BUNDLE}" -d ${CLASS_FILES_KOTLIN_BUNDLE} ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA_BUNDLE};
COMMENT "kotlinc (bundle)"
VERBATIM
)
set(CLASS_FILES_JAVA_BUNDLE "${CLASS_BUNDLE_FOLDER}/java.jar")
add_custom_command(
OUTPUT ${CLASS_FILES_JAVA_BUNDLE}
DEPENDS ${INPUTFILES_JAVA} ${R_JAVA_BUNDLE} ${CLASS_FILES_KOTLIN_BUNDLE}
COMMAND ${Java_JAVAC_EXECUTABLE} -Xlint:all --class-path "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN_BUNDLE}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_BUNDLE}" --source 8 --target 8 -d ${CLASS_BUNDLE_FOLDER}/java ${INPUTFILES_JAVA};
COMMAND ${Java_JAR_EXECUTABLE} --create --no-manifest --file ${CLASS_FILES_JAVA_BUNDLE} -C ${CLASS_BUNDLE_FOLDER}/java .;
COMMENT "javac + jar (bundle)"
VERBATIM
)
if(PROGUARD)
set(CLASSPATH_FOR_PROGUARD ${ANDROID_JAR})
if(KOTLIN_STDLIB)
# required at least for class kotlin.Metadata
set(CLASSPATH_FOR_PROGUARD "${CLASSPATH_FOR_PROGUARD}${CLASSPATH_SEPARATOR}${KOTLIN_STDLIB}")
endif()
set(OPTIMIZED_CLASS_BUNDLE ${CLASS_BUNDLE_FOLDER}/optimized.jar)
add_custom_command(
OUTPUT ${OPTIMIZED_CLASS_BUNDLE}
DEPENDS ${CLASS_FILES_KOTLIN_BUNDLE} ${CLASS_FILES_JAVA_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASS_BUNDLE_FOLDER}/optimized/
COMMAND ${PROGUARD} -dontobfuscate -libraryjars ${CLASSPATH_FOR_PROGUARD}
-injars ${CLASS_FILES_JAVA_BUNDLE}/${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN_BUNDLE}
-outjars ${OPTIMIZED_CLASS_BUNDLE}
-dontwarn org.jetbrains.annotations.NotNull # used only for static analysis, safe to ignore, no need to add other jars to classpath
# should be user-supplied; callback for jni
-keep "public class com.example.hello.HelloFromCPP{public static void log();}"
-include ${PROGUARDRULES_F_BUNDLE} # references com.example.MainActivity
COMMENT "optimize .class files (bundle)"
VERBATIM
)
set(CLASS_FILES_BUNDLE ${OPTIMIZED_CLASS_BUNDLE})
else()
set(CLASS_FILES_BUNDLE ${CLASS_FILES_KOTLIN_BUNDLE} ${CLASS_FILES_JAVA_BUNDLE})
endif()
# convert .class to dex
set(DEX_FILE_BUNDLE ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle/classes.dex)
if(D8)
add_custom_command(
OUTPUT ${DEX_FILE_BUNDLE}
DEPENDS ${MANIFEST} ${CLASS_FILES_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle
COMMAND ${D8} --output ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle/ --classpath ${ANDROID_JAR} ${CLASS_FILES_BUNDLE}
COMMENT "convert .class to .dex file (d8)"
VERBATIM
)
else()
add_custom_command(
OUTPUT ${DEX_FILE_BUNDLE}
DEPENDS ${MANIFEST} ${CLASS_FILES_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle
COMMAND ${DX} --dex --min-sdk-version=23 --output=${DEX_FILE_BUNDLE} ${CLASS_FILES_BUNDLE}
COMMENT "convert .class to .dex file (bundle)"
VERBATIM
)
endif()
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/base_for_bundle/resources.pb
DEPENDS ${DEX_FILE_BUNDLE} hello
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/base_for_bundle
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/output_/resources.pb ${CMAKE_BINARY_DIR}/base_for_bundle/resources.pb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/base_for_bundle/manifest
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/output_/AndroidManifest.xml ${CMAKE_BINARY_DIR}/base_for_bundle/manifest/AndroidManifest.xml
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/base_for_bundle/dex
COMMAND ${CMAKE_COMMAND} -E copy ${DEX_FILE_BUNDLE} ${CMAKE_BINARY_DIR}/base_for_bundle/dex/classes.dex
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/output_/res ${CMAKE_BINARY_DIR}/base_for_bundle/res
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_for_bundle/lib/${ANDROID_ABI}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libhello.so ${CMAKE_CURRENT_BINARY_DIR}/base_for_bundle/lib/${ANDROID_ABI}
COMMENT "collect resources for bundle"
VERBATIM
)
set(BASE_ZIP "${CMAKE_CURRENT_BINARY_DIR}/base.zip" )
add_custom_command( # separate command because of WORKING_DIRECTORY
OUTPUT ${BASE_ZIP}
DEPENDS ${CMAKE_BINARY_DIR}/base_for_bundle/resources.pb
COMMAND ${CMAKE_COMMAND} -E tar "cf" ${CMAKE_BINARY_DIR}/base.zip --format=zip -- .
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/base_for_bundle/
COMMENT "create base.zip"
VERBATIM
)
set(FINAL_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/app.aab")
add_custom_command(
OUTPUT ${FINAL_BUNDLE}
DEPENDS ${BASE_ZIP}
COMMAND ${BUNDLETOOL} build-bundle --modules=${BASE_ZIP} --output=${FINAL_BUNDLE}
# FIXME: is it possible to create only v2 signature?
COMMAND ${BUNDLETOOL} build-apks --bundle=${FINAL_BUNDLE}
--output=${CMAKE_CURRENT_BINARY_DIR}/app.apks
--mode=universal
--ks=${KEYSTORE}
--ks-pass pass:android
--ks-key-alias=androidkey
--key-pass pass:android
COMMAND ${CMAKE_COMMAND} -E tar x ${CMAKE_CURRENT_BINARY_DIR}/app.apks universal.apk
COMMENT "create final signed aab+apk"
VERBATIM
)
add_custom_target(aab ALL DEPENDS ${FINAL_BUNDLE})
Features of the CMake project
I put the focus on the project itself and not on the source files, but I tried to ensure that following works correctly:
-
packaging
.apk
-
packaging
.aab
-
create an
.apk
from an.aab
-
signing
-
use string and dialog as resources
-
a java function calling a kotlin function
-
a kotlin function calling a java function
-
compiling a native (in this case C++) library
-
a java function calling a C++ function
-
a C++ function calling a java function
-
reproducible artifacts (thankfully
aapt
andbundletool
do the Right Thing ™ by default) -
use proguard for optimizing generated
.class
files -
Use
d8
anddx
for creating.dex
files
Notable missing features
Things that I am aware are missing, there are surely ton of other things too:
-
handling multiple manifest and resource files, which might be required when integrating external libraries
-
creating
aar
package and.pom
file for consumption for Gradle projects -
the library
androidx
seems to provide basic classes, for exampleclass androidx.annotation.Keep
. Maybe it would be better to have it packaged? -
integration of
R8
, AFAIK it has not been packaged for Debian, similarly tobundletool
-
creating icons (for example from an
.svg
file) without Android Studio -
downloading direct and transitive dependencies
-
encapsulate the CMake custom commands in functions and targets as reusable blocks to be used in projects
-
create in one go a package for multiple platforms (ie set
ANDROID_ABI
toarm64-v8a
,x86
,x86_64
,armeabi
, andarmeabi-v7a
)
It would be nice if there would be a way to ensure that all relevant .class
file have been packaged. Since at the beginning I forgot to add the generated Kotlin file, and since the error only manifests itself at runtime, it would be much better if there would be at least a warning while building the application. There are some analyzers, but I did not try them out (yet).
Out of scope, but still important, would be some IDE integration. I’m not sure if Java-oriented IDE understand projects based on cmake
, make
, or ninja
; which means that developing and debugging will be harder than necessary.
Another important thing is that some tools are not packaged (r8
), and other are outdated (kotlin
). While it is possible to download and install them by hand, managing packages is generally easier. Even if one wants to use containers for handling development environment, doing and apt install …
inside a dockerfile is much easier that downloading and managing archives.
Full project
The full project can be downloaded as zip archive.
Here is an overview of the most relevant files
The native library:
#include <cstring>
#include <jni.h>
#include <cassert>
#include <android/log.h>
namespace{
void callback(JNIEnv* env) {
assert(env);
jclass clazz = env ? env->FindClass("com/example/hello/HelloFromCPP") : nullptr;
assert(clazz != nullptr);
jmethodID methodID = clazz ? env->GetStaticMethodID(clazz, "log", "()V") : nullptr;
assert(methodID != nullptr);
(methodID && clazz) ? env->CallStaticVoidMethod(clazz, methodID) : void();
}
}
extern "C" {
JNIEXPORT jstring Java_com_example_hello_HelloFromCPP_message( JNIEnv* env, jclass ) {
__android_log_write( ANDROID_LOG_ERROR, "hello.cpp", "use __android_log_write for logging" );
callback(env);
return env->NewStringUTF("Hello from C++");
}
}
The main activity; the entry point of this program:
package com.example;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import com.example.R;
import com.example.KotlinKt;
import com.example.hello.HelloFromCPP;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.e("cmakeapp", "fekir onCreate");
super.onCreate(savedInstanceState);
try{
String s = this.getString(R.string.made_with_cmake);
View view = LayoutInflater.from(this).inflate(R.layout.activity_main, null);
TextView textView = view.findViewById(R.id.my_text);
textView.setText(s + "\n "
+ KotlinKt.message() + "\n "
+ HelloFromCPP.message() + "\n "
);
setContentView(view);
} catch (Exception ex) {
TextView textView = new TextView(this);
textView.setText( "an error occurred while creating the view:\n\n" + ex.getMessage());
setContentView(textView);
}
}
static String name() {
return "MainActivity";
}
}
The interface to the native code, also responsible for loading the shared library.
package com.example.hello; import android.util.Log; public class HelloFromCPP { public native static String message(); // used as callback from JNI code public static void log(){ Log.e("cmakeapp", "callback from c++ code"); } static { Log.e("cmakeapp", "loadLibrary hello"); try{ System.loadLibrary("hello"); Log.e("cmakeapp", "libhello loaded"); } catch (Throwable ex){ Log.e("cmakeapp", "loadLibrary failed", ex); throw ex; } } }
A Kotlin source file:
package com.example
import com.example.MainActivity;
fun message(): String {
return "Hello from kotlin " + " for " + MainActivity.name();
}
The project file
cmake_minimum_required(VERSION 3.31)
project(AndroidApp)
set(CLASSPATH_SEPARATOR ":")
if(CMAKE_HOST_WIN32)
set(CLASSPATH_SEPARATOR ";") # unfortunately not possible to use ; on all platforms
endif()
if(NOT ANDROID_SDK_ROOT)
if($ENV{ANDROID_SDK_ROOT})
set(ANDROID_SDK_ROOT $ENV{ANDROID_SDK_ROOT} CACHE PATH "Path to Android SDK")
else()
set(ANDROID_SDK_ROOT "/usr/lib/android-sdk/" CACHE PATH "Path to Android SDK")
endif()
endif()
if(NOT BUILD_TOOLS)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/build-tools "${ANDROID_SDK_ROOT}/build-tools/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING)
set(BUILD_TOOLS)
foreach(child ${children})
if(IS_DIRECTORY ${ANDROID_SDK_ROOT}/build-tools/${child})
list(APPEND BUILD_TOOLS ${ANDROID_SDK_ROOT}/build-tools/${child})
endif()
endforeach()
endif()
find_program(AAPT NAMES aapt REQUIRED PATHS ${BUILD_TOOLS})
find_program(AAPT2 NAMES aapt2 REQUIRED PATHS ${BUILD_TOOLS})
find_program(APKSIGNER NAMES apksigner signapk REQUIRED PATHS ${BUILD_TOOLS})
find_program(ZIPALIGN NAMES zipalign REQUIRED PATHS ${BUILD_TOOLS})
execute_process(COMMAND "${ZIPALIGN}" OUTPUT_VARIABLE ZIPALIGN_OUTPUT ERROR_VARIABLE ZIPALIGN_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
string(FIND "${ZIPALIGN_OUTPUT}" " -P " position)
set(ZIPALIGN_ALIGNEMNT_ARG "-p")
if(position GREATER -1)
set(ZIPALIGN_ALIGNEMNT_ARG "-P" "16")
endif()
find_program(DX NAMES dx PATHS ${BUILD_TOOLS})
find_program(D8 NAMES d8 PATHS ${BUILD_TOOLS})
if(NOT DX AND NOT D8)
message(FATAL_ERROR "At least dx or d8 is required, none found in ${BUILD_TOOLS}")
endif()
find_program(KOTLINC NAMES kotlinc REQUIRED)
set(KOTLIN_STDLIB "/usr/share/java/kotlin-stdlib.jar")
find_program(PROGUARD NAMES proguard)
find_package(Java 1.8)
find_package(Java COMPONENTS Development)
include(UseJava) # java, jar, and other tools
set(BUNDLETOOL ${Java_JAVA_EXECUTABLE} -jar ${CMAKE_SOURCE_DIR}/bundletool-all-1.18.1.jar)
if(NOT ANDROID_PLATFORM_DIR)
file(GLOB children RELATIVE ${ANDROID_SDK_ROOT}/platforms "${ANDROID_SDK_ROOT}/platforms/*")
list(SORT children COMPARE NATURAL ORDER DESCENDING) # NOTE: also considers ${ANDROID_SDK_ROOT}/build-tools/debian
list(GET children 0 ANDROID_PLATFORM_DIR)
if(ANDROID_PLATFORM_DIR STRLESS ANDROID_PLATFORM)
message(FATAL_ERROR "ERROR: no valid platform found. Found ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR}, but it is less than configured minimum supported version: ${ANDROID_PLATFORM}")
endif()
set(ANDROID_PLATFORM_DIR ${ANDROID_SDK_ROOT}/platforms/${ANDROID_PLATFORM_DIR})
endif()
find_file(ANDROID_JAR
NAMES android.jar
PATHS ${ANDROID_PLATFORM_DIR} # with 36 get some warnings
REQUIRED NO_CMAKE_FIND_ROOT_PATH # both missing from find_jar
)
add_executable(main src/main.cpp)
add_library(hello SHARED
src/hello.cpp
)
target_link_libraries(hello
PRIVATE
log # required for logging with __android_log_write
)
set(CLASS_APP_FOLDER "${CMAKE_BINARY_DIR}/class_app_apk")
# PROJECT files
set(INPUTFILES_JAVA
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/MainActivity.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/Hello.java
${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/hello/HelloFromCPP.java
)
set(INPUTFILES_KOTLIN ${CMAKE_CURRENT_SOURCE_DIR}/src/com/example/kotlin.kt)
set(BASE_CLASSPATH_FOR_JAVAC "${ANDROID_JAR}")
if(KOTLIN_STDLIB)
# required at least for class kotlin.Metadata
set(BASE_CLASSPATH_FOR_JAVAC "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${KOTLIN_STDLIB}")
endif()
set(STRINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/values/strings.xml")
set(IDS "${CMAKE_CURRENT_SOURCE_DIR}/res/values/ids.xml")
set(LAYOUTS_XML "${CMAKE_CURRENT_SOURCE_DIR}/res/layout/activity_main.xml")
set(MANIFEST "${CMAKE_CURRENT_SOURCE_DIR}/src/AndroidManifest.xml")
# create APK directly
set(FINAL_APK "${CMAKE_CURRENT_BINARY_DIR}/app.apk")
# FIXME: iterate like java files
set(RESOURCES "${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml")
add_custom_command(
OUTPUT ${RESOURCES}
DEPENDS ${LAYOUTS_XML} ${STRINGS_XML}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/drawable/
COMMAND ${CMAKE_COMMAND} -E copy ${LAYOUTS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/layout/activity_main.xml
COMMAND ${CMAKE_COMMAND} -E copy ${STRINGS_XML} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/strings.xml
COMMAND ${CMAKE_COMMAND} -E copy ${IDS} ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk/values/ids.xml
COMMENT "copy relevant resources to build folder"
VERBATIM
)
set(CLASSPATH_GEN_APP ${CMAKE_CURRENT_BINARY_DIR}/gen_app_apk/)
set(PROGUARDRULES_D_APP ${CMAKE_CURRENT_BINARY_DIR}/proguard_rules/)
set(PROGUARDRULES_F_APP ${PROGUARDRULES_D_APP}/aapt.pro)
set(R_JAVA ${CLASSPATH_GEN_APP}/com/example/R.java)
add_custom_command(
OUTPUT ${R_JAVA} ${PROGUARDRULES_F_APP}
DEPENDS ${MANIFEST} ${STRINGS_XML} ${LAYOUTS_XML} ${RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASSPATH_GEN_APP}
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROGUARDRULES_D_APP}
COMMAND ${AAPT} package -f -m -J ${CLASSPATH_GEN_APP} -S ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk -G ${PROGUARDRULES_F_APP} -M ${MANIFEST} -I ${ANDROID_JAR}
COMMENT "create R.java file with aapt"
VERBATIM
CODEGEN
)
set(CLASS_FILES_KOTLIN ${CLASS_APP_FOLDER}/kotlin.jar)
add_custom_command(
OUTPUT ${CLASS_FILES_KOTLIN}
DEPENDS ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASS_APP_FOLDER} # not required when not creating jar file
COMMAND ${KOTLINC} -jvm-target 1.8 -classpath "${ANDROID_JAR}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_APP}" -d ${CLASS_FILES_KOTLIN} ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA};
COMMENT "kotlinc"
VERBATIM
)
# java might depend on .class files generated by kotlin
set(CLASS_FILES_JAVA ${CLASS_APP_FOLDER}/java.jar)
add_custom_command(
OUTPUT ${CLASS_FILES_JAVA}
DEPENDS ${INPUTFILES_JAVA} ${R_JAVA} ${CLASS_FILES_KOTLIN}
COMMAND ${Java_JAVAC_EXECUTABLE} -Xlint:all --class-path "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_APP}" --source 8 --target 8 -d ${CLASS_APP_FOLDER}/java ${INPUTFILES_JAVA};
COMMAND ${Java_JAR_EXECUTABLE} --date "1980-01-01T12:01:00Z" --create --no-manifest --file ${CLASS_FILES_JAVA} -C ${CLASS_APP_FOLDER}/java .;
COMMENT "javac + jar"
VERBATIM
)
if(PROGUARD)
set(CLASSPATH_FOR_PROGUARD ${ANDROID_JAR})
if(CLASS_FILES_KOTLIN)
# required at least for class kotlin.Metadata
set(CLASSPATH_FOR_PROGUARD "${CLASSPATH_FOR_PROGUARD}${CLASSPATH_SEPARATOR}/usr/share/java/kotlin-stdlib.jar")
endif()
set(OPTIMIZED_CLASS ${CLASS_APP_FOLDER}/optimized.jar)
add_custom_command(
OUTPUT ${OPTIMIZED_CLASS}
DEPENDS ${CLASS_FILES_KOTLIN} ${CLASS_FILES_JAVA}
COMMAND ${PROGUARD} -dontobfuscate -libraryjars ${CLASSPATH_FOR_PROGUARD}
-injars ${CLASS_FILES_JAVA}${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN}
-outjars ${OPTIMIZED_CLASS}
-dontwarn org.jetbrains.annotations.NotNull # used only for static analysis, safe to ignore, no need to add other jars to classpath
# should be user-supplied; callback for jni
-keep "public class com.example.hello.HelloFromCPP{public static void log();}"
-include ${PROGUARDRULES_F_APP} # references com.example.MainActivity
COMMENT "optimize .class files"
VERBATIM
)
set(CLASS_FILES ${OPTIMIZED_CLASS})
else()
set(CLASS_FILES ${CLASS_FILES_KOTLIN} ${CLASS_FILES_JAVA})
endif()
# convert .class to dex; note that DX is not part of never apks
set(DEX_FILE ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/classes.dex)
if(D8) # prefer d8 to dx if available
add_custom_command(
OUTPUT ${DEX_FILE}
DEPENDS ${MANIFEST} ${CLASS_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk
COMMAND ${D8} --output ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/ --classpath ${ANDROID_JAR} ${CLASS_FILES}
COMMENT "convert .class to .dex file (d8)"
VERBATIM
)
else()
add_custom_command(
OUTPUT ${DEX_FILE}
DEPENDS ${MANIFEST} ${CLASS_FILES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk
COMMAND ${DX} --dex --min-sdk-version=23 --output=${DEX_FILE} ${CLASS_FILES}
COMMENT "convert .class to .dex file (dx)"
VERBATIM
)
endif()
set(UNSIGNED_APK "${CMAKE_CURRENT_BINARY_DIR}/app.unsigned.apk")
add_custom_command(
OUTPUT ${UNSIGNED_APK}
DEPENDS ${MANIFEST} ${DEX_FILE} hello ${RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/lib/${ANDROID_ABI}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libhello.so ${CMAKE_CURRENT_BINARY_DIR}/base_app_apk/lib/${ANDROID_ABI}
COMMAND ${AAPT} package -f -F ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk -I ${ANDROID_JAR} -M ${MANIFEST} -S ${CMAKE_CURRENT_BINARY_DIR}/res_app_apk base_app_apk
COMMAND ${ZIPALIGN} ${ZIPALIGN_ALIGNEMNT_ARG} -f 4 ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk ${UNSIGNED_APK}
COMMAND ${CMAKE_COMMAND} -E remove ${CMAKE_CURRENT_BINARY_DIR}/app.unaligned.apk
COMMENT "create unsigned apk"
VERBATIM
)
set(KEYSTORE "${CMAKE_CURRENT_SOURCE_DIR}/keystore.jks")
add_custom_command(
OUTPUT ${FINAL_APK}
DEPENDS ${UNSIGNED_APK}
COMMAND ${ZIPALIGN} ${ZIPALIGN_ALIGNEMNT_ARG} -f 4 ${UNSIGNED_APK} ${FINAL_APK}
COMMAND ${APKSIGNER} sign --in ${FINAL_APK} -ks ${KEYSTORE} --ks-key-alias androidkey --ks-pass pass:android --key-pass pass:android --min-sdk-version 21 --v2-signing-enabled true --v1-signing-enabled false
COMMENT "create final signed apk"
VERBATIM
)
# generate bundle
set(FLAT_RESOURCES ${CMAKE_BINARY_DIR}/flat_res/layout_activity_main.xml.flat ${CMAKE_BINARY_DIR}/flat_res/values_strings.arsc.flat ${CMAKE_BINARY_DIR}/flat_res/values_ids.arsc.flat)
add_custom_command(
OUTPUT ${FLAT_RESOURCES}
DEPENDS ${STRINGS_XML} ${IDS} ${LAYOUTS_XML}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/flat_res # otherwise aapt2 creates zip archive
COMMAND ${AAPT2} compile -o ${CMAKE_BINARY_DIR}/flat_res/ ${STRINGS_XML} ${IDS} ${LAYOUTS_XML}
COMMENT "compile resources to flat"
VERBATIM
)
set(CLASSPATH_GEN_BUNDLE ${CMAKE_CURRENT_BINARY_DIR}/gen_app_bundle/)
set(R_JAVA_BUNDLE ${CLASSPATH_GEN_BUNDLE}/com/example/R.java)
set(PROGUARDRULES_D_BUNDLE ${CMAKE_CURRENT_BINARY_DIR}/proguard_rules_bundle/)
set(PROGUARDRULES_F_BUNDLE ${PROGUARDRULES_D_BUNDLE}/aapt.pro)
add_custom_command(
OUTPUT ${R_JAVA_BUNDLE} ${PROGUARDRULES_F_BUNDLE}
DEPENDS ${FLAT_RESOURCES}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/output_ # otherwise aapt2 link errors
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASSPATH_GEN_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROGUARDRULES_D_BUNDLE}
COMMAND ${AAPT2} link --proto-format -o ${CMAKE_BINARY_DIR}/output_
-I ${ANDROID_JAR}
--manifest ${MANIFEST}
-R ${FLAT_RESOURCES}
--output-to-dir
--java ${CLASSPATH_GEN_BUNDLE}/
--proguard ${PROGUARDRULES_F_BUNDLE}
COMMENT "aapt2 link and create R.java"
VERBATIM
CODEGEN
)
set(CLASS_BUNDLE_FOLDER "${CMAKE_BINARY_DIR}/class_app_bundle")
set(CLASS_FILES_KOTLIN_BUNDLE "${CLASS_BUNDLE_FOLDER}/kotlin.jar")
add_custom_command(
OUTPUT ${CLASS_FILES_KOTLIN_BUNDLE}
DEPENDS ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASS_BUNDLE_FOLDER} # not required when not creating jar file
COMMAND ${KOTLINC} -jvm-target 1.8 -classpath "${ANDROID_JAR}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_BUNDLE}" -d ${CLASS_FILES_KOTLIN_BUNDLE} ${INPUTFILES_KOTLIN} ${INPUTFILES_JAVA} ${R_JAVA_BUNDLE};
COMMENT "kotlinc (bundle)"
VERBATIM
)
set(CLASS_FILES_JAVA_BUNDLE "${CLASS_BUNDLE_FOLDER}/java.jar")
add_custom_command(
OUTPUT ${CLASS_FILES_JAVA_BUNDLE}
DEPENDS ${INPUTFILES_JAVA} ${R_JAVA_BUNDLE} ${CLASS_FILES_KOTLIN_BUNDLE}
COMMAND ${Java_JAVAC_EXECUTABLE} -Xlint:all --class-path "${BASE_CLASSPATH_FOR_JAVAC}${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN_BUNDLE}${CLASSPATH_SEPARATOR}${CLASSPATH_GEN_BUNDLE}" --source 8 --target 8 -d ${CLASS_BUNDLE_FOLDER}/java ${INPUTFILES_JAVA};
COMMAND ${Java_JAR_EXECUTABLE} --create --no-manifest --file ${CLASS_FILES_JAVA_BUNDLE} -C ${CLASS_BUNDLE_FOLDER}/java .;
COMMENT "javac + jar (bundle)"
VERBATIM
)
if(PROGUARD)
set(CLASSPATH_FOR_PROGUARD ${ANDROID_JAR})
if(KOTLIN_STDLIB)
# required at least for class kotlin.Metadata
set(CLASSPATH_FOR_PROGUARD "${CLASSPATH_FOR_PROGUARD}${CLASSPATH_SEPARATOR}${KOTLIN_STDLIB}")
endif()
set(OPTIMIZED_CLASS_BUNDLE ${CLASS_BUNDLE_FOLDER}/optimized.jar)
add_custom_command(
OUTPUT ${OPTIMIZED_CLASS_BUNDLE}
DEPENDS ${CLASS_FILES_KOTLIN_BUNDLE} ${CLASS_FILES_JAVA_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CLASS_BUNDLE_FOLDER}/optimized/
COMMAND ${PROGUARD} -dontobfuscate -libraryjars ${CLASSPATH_FOR_PROGUARD}
-injars ${CLASS_FILES_JAVA_BUNDLE}/${CLASSPATH_SEPARATOR}${CLASS_FILES_KOTLIN_BUNDLE}
-outjars ${OPTIMIZED_CLASS_BUNDLE}
-dontwarn org.jetbrains.annotations.NotNull # used only for static analysis, safe to ignore, no need to add other jars to classpath
# should be user-supplied; callback for jni
-keep "public class com.example.hello.HelloFromCPP{public static void log();}"
-include ${PROGUARDRULES_F_BUNDLE} # references com.example.MainActivity
COMMENT "optimize .class files (bundle)"
VERBATIM
)
set(CLASS_FILES_BUNDLE ${OPTIMIZED_CLASS_BUNDLE})
else()
set(CLASS_FILES_BUNDLE ${CLASS_FILES_KOTLIN_BUNDLE} ${CLASS_FILES_JAVA_BUNDLE})
endif()
# convert .class to dex
set(DEX_FILE_BUNDLE ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle/classes.dex)
if(D8)
add_custom_command(
OUTPUT ${DEX_FILE_BUNDLE}
DEPENDS ${MANIFEST} ${CLASS_FILES_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle
COMMAND ${D8} --output ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle/ --classpath ${ANDROID_JAR} ${CLASS_FILES_BUNDLE}
COMMENT "convert .class to .dex file (d8)"
VERBATIM
)
else()
add_custom_command(
OUTPUT ${DEX_FILE_BUNDLE}
DEPENDS ${MANIFEST} ${CLASS_FILES_BUNDLE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_app_bundle
COMMAND ${DX} --dex --min-sdk-version=23 --output=${DEX_FILE_BUNDLE} ${CLASS_FILES_BUNDLE}
COMMENT "convert .class to .dex file (bundle)"
VERBATIM
)
endif()
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/base_for_bundle/resources.pb
DEPENDS ${DEX_FILE_BUNDLE} hello
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/base_for_bundle
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/output_/resources.pb ${CMAKE_BINARY_DIR}/base_for_bundle/resources.pb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/base_for_bundle/manifest
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/output_/AndroidManifest.xml ${CMAKE_BINARY_DIR}/base_for_bundle/manifest/AndroidManifest.xml
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/base_for_bundle/dex
COMMAND ${CMAKE_COMMAND} -E copy ${DEX_FILE_BUNDLE} ${CMAKE_BINARY_DIR}/base_for_bundle/dex/classes.dex
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/output_/res ${CMAKE_BINARY_DIR}/base_for_bundle/res
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/base_for_bundle/lib/${ANDROID_ABI}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libhello.so ${CMAKE_CURRENT_BINARY_DIR}/base_for_bundle/lib/${ANDROID_ABI}
#COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/dex/lib ${CMAKE_BINARY_DIR}/base_for_bundle/lib
# roots
# assets
#COMMAND ${CMAKE_COMMAND} -E tar "cf" ${CMAKE_BINARY_DIR}/base.zip --format=zip -- ${CMAKE_BINARY_DIR}/base_for_bundle
COMMENT "collect resources for bundle"
VERBATIM
)
set(BASE_ZIP "${CMAKE_CURRENT_BINARY_DIR}/base.zip" )
add_custom_command( # separate command because of WORKING_DIRECTORY
OUTPUT ${BASE_ZIP}
DEPENDS ${CMAKE_BINARY_DIR}/base_for_bundle/resources.pb
#COMMAND ${CMAKE_COMMAND} -E tar "cf" ${CMAKE_BINARY_DIR}/base.zip --format=zip -- dex lib manifest res
COMMAND ${CMAKE_COMMAND} -E tar "cf" ${CMAKE_BINARY_DIR}/base.zip --format=zip -- .
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/base_for_bundle/
COMMENT "create base.zip"
VERBATIM
)
set(FINAL_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/app.aab")
add_custom_command(
OUTPUT ${FINAL_BUNDLE}
DEPENDS ${BASE_ZIP}
COMMAND ${BUNDLETOOL} build-bundle --modules=${BASE_ZIP} --output=${FINAL_BUNDLE}
COMMAND ${BUNDLETOOL} build-apks --bundle=${FINAL_BUNDLE}
--output=${CMAKE_CURRENT_BINARY_DIR}/app.apks
--mode=universal
--ks=${KEYSTORE}
--ks-pass pass:android
--ks-key-alias=androidkey
--key-pass pass:android
COMMAND ${CMAKE_COMMAND} -E tar x ${CMAKE_CURRENT_BINARY_DIR}/app.apks universal.apk
COMMENT "create final signed aab+apk"
VERBATIM
)
add_custom_target(apk ALL DEPENDS ${FINAL_APK})
add_custom_target(aab ALL DEPENDS ${FINAL_BUNDLE})
Last but not least, the resource files and manifest.
Note that this manifest also depends on the resource (the resource @string/app_name
):
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example"
android:versionCode="1"
android:versionName="1.0">
<!-- FIXME: those values should be determined by the build system -->
<uses-sdk
android:minSdkVersion="23"
android:targetSdkVersion="35"
maxSdkVersion="35"
/>
<application
android:label="@string/app_name"
android:debuggable="true"
android:allowBackup="true"
>
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">CMakeApp</string>
<string name="made_with_cmake">Made with CMake</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/my_text"
/>
</LinearLayout>
If you have questions, comments, or found typos, the notes are not clear, or there are some errors; then just contact me.