From 9cd0167c7e79ff47a8b7926d48902a3e2f9792a5 Mon Sep 17 00:00:00 2001 From: Vasilis Nicolaou Date: Wed, 8 Oct 2025 12:32:44 +0300 Subject: [PATCH 1/5] Android: Test apk size reduction - basic idea --- libs/androidlib/src/mill/androidlib/AndroidAppModule.scala | 7 +++++-- libs/androidlib/src/mill/androidlib/AndroidModule.scala | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala index 9ee05478fb45..e8a78c4db4f9 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala @@ -896,6 +896,9 @@ trait AndroidAppModule extends AndroidModule { outer => override def androidIsDebug: T[Boolean] = Task { true } + override def moduleDeps: Seq[JavaModule] = Seq.empty + override def compileModuleDeps: Seq[JavaModule] = Seq(outer) + override def resolutionParams: Task[ResolutionParams] = Task.Anon(outer.resolutionParams()) override def androidApplicationId: String = s"${outer.androidApplicationId}.test" @@ -1063,7 +1066,7 @@ trait AndroidAppModule extends AndroidModule { outer => * as its apk is installed separately */ def androidTransitiveTestClasspath: T[Seq[PathRef]] = Task { - Task.traverse(transitiveModuleCompileModuleDeps) { + Task.traverse(transitiveRunModuleDeps) { m => Task.Anon(m.localRunClasspath()) }().flatten @@ -1071,7 +1074,7 @@ trait AndroidAppModule extends AndroidModule { outer => /** The instrumented dex should just contain the test dependencies and locally tested files */ override def androidPackagedClassfiles: T[Seq[PathRef]] = Task { - (testClasspath() ++ androidTransitiveTestClasspath()) + androidTransitiveTestClasspath() .map(_.path).filter(os.isDir) .flatMap(os.walk(_)) .filter(os.isFile) diff --git a/libs/androidlib/src/mill/androidlib/AndroidModule.scala b/libs/androidlib/src/mill/androidlib/AndroidModule.scala index fa19291a3d3b..703456b99857 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidModule.scala @@ -193,11 +193,11 @@ trait AndroidModule extends JavaModule { outer => /** * Gets all the compiled Android resources (typically in res/ directory) - * from the [[transitiveModuleCompileModuleDeps]] + * from the [[transitiveModuleRunModuleDeps]] * @return a sequence of PathRef to the compiled resources */ def androidTransitiveCompiledResources: T[Seq[PathRef]] = Task { - Task.traverse(transitiveModuleCompileModuleDeps) { + Task.traverse(transitiveModuleRunModuleDeps) { case m: AndroidModule => Task.Anon(m.androidCompiledModuleResources()) case _ => From 786f75c62065b9e8b133a8bd9dbd3e46fc3a63ca Mon Sep 17 00:00:00 2001 From: Vasilis Nicolaou Date: Fri, 10 Oct 2025 14:23:06 +0300 Subject: [PATCH 2/5] Fix missing compile only parameters for R8 -> androidTest slim working --- .../mill/androidlib/AndroidAppModule.scala | 2 +- .../mill/androidlib/AndroidR8AppModule.scala | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala index e8a78c4db4f9..932b5802336e 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala @@ -1083,7 +1083,7 @@ trait AndroidAppModule extends AndroidModule { outer => } override def androidPackagedDeps: T[Seq[PathRef]] = Task { - androidResolvedMvnDeps() + androidResolvedRunMvnDeps() } /** diff --git a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala index 857052417cdd..cf50929e7a24 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala @@ -54,7 +54,7 @@ trait AndroidR8AppModule extends AndroidAppModule { } def androidLibraryProguardConfigs: Task[Seq[PathRef]] = Task { - androidUnpackArchives() + androidUnpackRunArchives() // TODO need also collect rules from other modules, // but Android lib module doesn't yet exist .flatMap(_.proguardRules) @@ -71,6 +71,27 @@ trait AndroidR8AppModule extends AndroidAppModule { androidDefaultProguardFiles() ++ androidProjectProguardFiles() ++ androidLibraryProguardConfigs() } + /** + * Creates a file for letting know R8 that [[compileModuleDeps]] and + * [[compileMvnDeps]] are in compile classpath only and not packaged with the apps. + * Useful for dependencies that are provided in devices and compile only module deps + * such as for avoiding to package main sources in the androidTest apk. + */ + def androidR8CompileOnlyClasspath: T[Option[PathRef]] = Task { + val resolvedCompileMvnDeps = + androidResolvedCompileMvnDeps() ++ upstreamCompileOutput().map(_.classes) + if (!resolvedCompileMvnDeps.isEmpty) { + val compiledMvnDepsFile = Task.dest / "compile-only-classpath.txt" + os.write.over( + compiledMvnDepsFile, + resolvedCompileMvnDeps.map(_.path.toString()).mkString("\n") + ) + Some(PathRef(compiledMvnDepsFile)) + } else + None + + } + /** Concatenates all rules into one file */ override def androidProguard: T[PathRef] = Task { val inheritedProguardFile = super.androidProguard() @@ -242,18 +263,14 @@ trait AndroidR8AppModule extends AndroidAppModule { r8ArgsBuilder ++= pgArgs - val resolvedCompileMvnDeps = androidResolvedCompileMvnDeps() - if (!resolvedCompileMvnDeps.isEmpty) { - val compiledMvnDepsFile = Task.dest / "compiled-mvndeps.txt" - os.write.over( - compiledMvnDepsFile, - androidResolvedCompileMvnDeps().map(_.path.toString()).mkString("\n") - ) - r8ArgsBuilder ++= Seq( + val compileOnlyClasspath = androidR8CompileOnlyClasspath() + + r8ArgsBuilder ++= compileOnlyClasspath.toSeq.flatMap(compiledMvnDepsFile => + Seq( "--classpath", - "@" + compiledMvnDepsFile.toString + "@" + compiledMvnDepsFile.path.toString ) - } + ) r8ArgsBuilder ++= androidR8Args() From df6c74791ee8f40ce503afb0c6d2a26ccc22ad93 Mon Sep 17 00:00:00 2001 From: Vasilis Nicolaou Date: Fri, 10 Oct 2025 15:05:20 +0300 Subject: [PATCH 3/5] Fixes for JetNews --- .../mill/androidlib/AndroidAppModule.scala | 22 ++++++++++--------- .../src/mill/androidlib/AndroidModule.scala | 11 +++++++++- .../mill/androidlib/AndroidR8AppModule.scala | 12 +++------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala index 932b5802336e..1b99b692468e 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala @@ -822,7 +822,7 @@ trait AndroidAppModule extends AndroidModule { outer => } - def knownProguardRules: T[String] = Task { + def androidKnownProguardRules: T[String] = Task { // TODO need also pick proguard files from // [[moduleDeps]] androidUnpackArchives() @@ -833,15 +833,13 @@ trait AndroidAppModule extends AndroidModule { outer => .mkString("\n") } - override def androidProguard: T[PathRef] = Task { - val inheritedProguardFile = super.androidProguard() - val proguardFile = Task.dest / "proguard-rules.pro" - - os.write(proguardFile, os.read(inheritedProguardFile.path)) - - os.write.append(proguardFile, knownProguardRules()) - - PathRef(proguardFile) + /** + * File names that are provided by the Android SDK in `androidSdkModule().androidProguardPath().path` + * + * For now, it's only used by [[AndroidR8AppModule]] + */ + def androidDefaultProguardFileNames: Task[Seq[String]] = Task.Anon { + Seq.empty[String] } // uses the d8 tool to generate the dex file, when minification is disabled @@ -916,6 +914,10 @@ trait AndroidAppModule extends AndroidModule { outer => def androidResources: T[Seq[PathRef]] = Task.Sources("src/androidTest/res") + override def androidDefaultProguardFileNames: Task[Seq[String]] = Task.Anon { + outer.androidDefaultProguardFileNames() + } + override def testFramework: T[String] = Task { "androidx.test.runner.AndroidJUnitRunner" } diff --git a/libs/androidlib/src/mill/androidlib/AndroidModule.scala b/libs/androidlib/src/mill/androidlib/AndroidModule.scala index 703456b99857..eea78c6d2081 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidModule.scala @@ -264,7 +264,7 @@ trait AndroidModule extends JavaModule { outer => override def compileClasspath: T[Seq[PathRef]] = Task { // TODO process metadata shipped with Android libs. It can have some rules with Target SDK, for example. // TODO support baseline profiles shipped with Android libs. - androidDepsClasspath() ++ androidTransitiveLibRClasspath() + androidDepsClasspath() ++ androidTransitiveLibRClasspath() ++ androidTransitiveModuleRClasspath() } /** @@ -522,6 +522,15 @@ trait AndroidModule extends JavaModule { outer => }().flatten } + def androidTransitiveModuleRClasspath: T[Seq[PathRef]] = Task { + Task.traverse(compileModuleDepsChecked) { + case m: AndroidModule => + Task.Anon(Seq(m.androidProcessedResources())) + case _ => + Task.Anon(Seq.empty[PathRef]) + }().flatten + } + /** * Namespace of the Android module. * Used in manifest package and also used as the package to place the generated R sources diff --git a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala index cf50929e7a24..39ab75e5c996 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala @@ -79,7 +79,9 @@ trait AndroidR8AppModule extends AndroidAppModule { */ def androidR8CompileOnlyClasspath: T[Option[PathRef]] = Task { val resolvedCompileMvnDeps = - androidResolvedCompileMvnDeps() ++ upstreamCompileOutput().map(_.classes) + androidResolvedCompileMvnDeps() ++ upstreamCompileOutput().map( + _.classes + ) ++ androidTransitiveModuleRClasspath() if (!resolvedCompileMvnDeps.isEmpty) { val compiledMvnDepsFile = Task.dest / "compile-only-classpath.txt" os.write.over( @@ -119,14 +121,6 @@ trait AndroidR8AppModule extends AndroidAppModule { ) } - /** - * File names that are provided by the Android SDK in `androidSdkModule().androidProguardPath().path` - * @return - */ - def androidDefaultProguardFileNames: Task[Seq[String]] = Task.Anon { - Seq.empty[String] - } - private def androidDefaultProguardFiles: Task[Seq[PathRef]] = Task.Anon { val dest = Task.dest androidDefaultProguardFileNames().map { fileName => From d6c1bb75bfc03a495702db55336196efd0016528 Mon Sep 17 00:00:00 2001 From: Vasilis Nicolaou Date: Fri, 10 Oct 2025 15:51:23 +0300 Subject: [PATCH 4/5] Androidtodo fixes --- libs/androidlib/src/mill/androidlib/AndroidModule.scala | 9 +++++++++ .../src/mill/androidlib/AndroidR8AppModule.scala | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/androidlib/src/mill/androidlib/AndroidModule.scala b/libs/androidlib/src/mill/androidlib/AndroidModule.scala index eea78c6d2081..c2fad558330f 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidModule.scala @@ -531,6 +531,15 @@ trait AndroidModule extends JavaModule { outer => }().flatten } + def androidTransitiveCompileOnlyClasspath: T[Seq[PathRef]] = Task { + Task.traverse(compileModuleDepsChecked) { + case m: AndroidModule => + Task.Anon(Seq(m.compile().classes)) + case _ => + Task.Anon(Seq.empty[PathRef]) + }().flatten + } + /** * Namespace of the Android module. * Used in manifest package and also used as the package to place the generated R sources diff --git a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala index 39ab75e5c996..baead298122c 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala @@ -79,9 +79,7 @@ trait AndroidR8AppModule extends AndroidAppModule { */ def androidR8CompileOnlyClasspath: T[Option[PathRef]] = Task { val resolvedCompileMvnDeps = - androidResolvedCompileMvnDeps() ++ upstreamCompileOutput().map( - _.classes - ) ++ androidTransitiveModuleRClasspath() + androidResolvedCompileMvnDeps() ++ androidTransitiveCompileOnlyClasspath() ++ androidTransitiveModuleRClasspath() if (!resolvedCompileMvnDeps.isEmpty) { val compiledMvnDepsFile = Task.dest / "compile-only-classpath.txt" os.write.over( From f45ce33c4985dd16b4c886496368a864f1a0f05e Mon Sep 17 00:00:00 2001 From: Vasilis Nicolaou Date: Fri, 10 Oct 2025 16:35:07 +0300 Subject: [PATCH 5/5] Minor dependency cleanup --- example/thirdparty/androidtodo/build.mill | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/example/thirdparty/androidtodo/build.mill b/example/thirdparty/androidtodo/build.mill index faa0c67ddb25..4704d379c8c9 100644 --- a/example/thirdparty/androidtodo/build.mill +++ b/example/thirdparty/androidtodo/build.mill @@ -133,7 +133,6 @@ object app def mvnDeps = super.mvnDeps() ++ composeDeps ++ Seq( mvn"junit:junit:4.13.2", mvn"androidx.arch.core:core-testing:2.2.0", - mvn"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0", mvn"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0", mvn"androidx.navigation:navigation-testing:2.8.5", mvn"androidx.test.espresso:espresso-core:3.6.1", @@ -185,7 +184,6 @@ object app // Dependencies for Android unit tests mvn"junit:junit:4.13.2", mvn"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0", - mvn"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0", // AndroidX Test - Instrumented testing mvn"androidx.test:core-ktx:1.6.1", mvn"androidx.test.ext:junit-ktx:1.2.1", @@ -227,7 +225,7 @@ object `shared-test` extends AndroidKotlinModule, AndroidHiltSupport { def androidEnableCompose = true - def androidNamespace = "com.example.android.architecture.blueprints.todoapp.daemon.test" + def androidNamespace = "com.example.android.architecture.blueprints.todoapp.shared.test" def kotlinSymbolProcessors: T[Seq[Dep]] = Seq( mvn"androidx.room:room-compiler:2.7.1", @@ -237,7 +235,6 @@ object `shared-test` extends AndroidKotlinModule, AndroidHiltSupport { def mvnDeps = super.mvnDeps() ++ Seq( mvn"junit:junit:4.13.2", mvn"androidx.arch.core:core-testing:2.2.0", - mvn"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0", mvn"org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0", mvn"androidx.test:core-ktx:1.6.1", mvn"androidx.test.ext:junit-ktx:1.2.1",