diff --git a/example/androidlib/java/5-R8/build.mill b/example/androidlib/java/5-R8/build.mill index 1068a37da0b..efe13425881 100644 --- a/example/androidlib/java/5-R8/build.mill +++ b/example/androidlib/java/5-R8/build.mill @@ -24,7 +24,7 @@ object androidSdkModule0 extends AndroidSdkModule { // <1> def buildToolsVersion = "35.0.0" } -object app extends AndroidAppModule { // <2> +object app extends AndroidR8AppModule { // <2> def androidSdkModule = mill.define.ModuleRef(androidSdkModule0) def androidMinSdk = 19 def androidCompileSdk = 35 @@ -61,7 +61,8 @@ object app extends AndroidAppModule { // <2> } // Instrumented tests (runs on emulator) - object it extends AndroidAppInstrumentedTests with AndroidTestModule.AndroidJUnit { + object it extends AndroidAppInstrumentedTests with AndroidR8AppModule + with AndroidTestModule.AndroidJUnit { def androidSdkModule = mill.define.ModuleRef(androidSdkModule0) override def androidIsDebug: T[Boolean] = Task { @@ -69,13 +70,13 @@ object app extends AndroidAppModule { // <2> } override def androidReleaseSettings: T[AndroidBuildTypeSettings] = Task { - AndroidBuildTypeSettings(isMinifyEnabled = false) + AndroidBuildTypeSettings(isMinifyEnabled = false).withProguardLocalFiles( + Seq( + moduleDir / "test-proguard-rules.pro" + ) + ) } - /* TODO currently the dependency resolution ignores the platform type and kotlinx-coroutines-core has - * conflicting classes with kotlinx-coroutines-core-jvm . Remove the exclusions once the dependency - * resolution resolves conflicts between androidJvm and jvm platform types - */ def mvnDeps = super.mvnDeps() ++ Seq( mvn"androidx.test.ext:junit:1.2.1", mvn"androidx.test:runner:1.6.2", diff --git a/example/androidlib/kotlin/1-hello-kotlin/build.mill b/example/androidlib/kotlin/1-hello-kotlin/build.mill index 1217b08fff5..9557d21b815 100644 --- a/example/androidlib/kotlin/1-hello-kotlin/build.mill +++ b/example/androidlib/kotlin/1-hello-kotlin/build.mill @@ -43,16 +43,6 @@ object app extends AndroidAppKotlinModule { override def androidIsDebug: T[Boolean] = Task { false } - override def androidReleaseSettings: T[AndroidBuildTypeSettings] = Task { - super.androidReleaseSettings() - .withDefaultProguardFile("proguard-android.txt") - .withProguardLocalFiles( - Seq( - moduleDir / "proguard-rules.pro" - ) - ) - } - object test extends AndroidAppKotlinTests with TestModule.Junit4 { def junit4Version = "4.13.2" } @@ -68,14 +58,6 @@ object app extends AndroidAppKotlinModule { false } - override def androidReleaseSettings: T[AndroidBuildTypeSettings] = Task { - AndroidBuildTypeSettings(isMinifyEnabled = false) - } - - /* TODO currently the dependency resolution ignores the platform type and kotlinx-coroutines-core has - * conflicting classes with kotlinx-coroutines-core-jvm . Remove the exclusions once the dependency - * resolution resolves conflicts between androidJvm and jvm platform types - */ def mvnDeps = super.mvnDeps() ++ Seq( mvn"androidx.test.ext:junit:1.2.1", mvn"androidx.test:runner:1.6.2", diff --git a/example/thirdparty/androidtodo/build.mill b/example/thirdparty/androidtodo/build.mill index 13c6a256bac..5e20431d6cf 100644 --- a/example/thirdparty/androidtodo/build.mill +++ b/example/thirdparty/androidtodo/build.mill @@ -18,12 +18,30 @@ object androidSdkModule0 extends AndroidSdkModule { } // Mill configuration for the Android Todo App project. -object app extends AndroidAppKotlinModule with AndroidBuildConfig with AndroidHiltSupport { +object app extends AndroidAppKotlinModule with AndroidR8AppModule with AndroidBuildConfig + with AndroidHiltSupport { def kotlinVersion = Versions.kotlinVersion def kotlinLanguageVersion = Versions.kotlinLanguageVersion def kspVersion = Versions.kspVersion + override def androidDebugSettings: T[AndroidBuildTypeSettings] = Task { + AndroidBuildTypeSettings( + isMinifyEnabled = false, + isShrinkEnabled = false + ).withDefaultProguardFile("proguard-android.txt") + .withProguardLocalFiles( + Seq( + moduleDir / "proguard-rules.pro" + ) + ) + } + + // TODO consider using a debug module + def debugSources = Task.Sources("src/debug/java") + + override def sources = super.sources() ++ debugSources() + def androidApplicationNamespace = "com.example.android.architecture.blueprints.todoapp" // TODO change this to com.example.android.architecture.blueprints.main when mill supports build variants def androidApplicationId = "com.example.android.architecture.blueprints.main" @@ -42,6 +60,7 @@ object app extends AndroidAppKotlinModule with AndroidBuildConfig with AndroidHi mvn"androidx.core:core-ktx:1.15.0", mvn"androidx.appcompat:appcompat:1.7.0", mvn"androidx.annotation:annotation:1.9.1", + mvn"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0", mvn"com.jakewharton.timber:timber:5.0.1", mvn"androidx.test.espresso:espresso-idling-resource:3.6.1", mvn"androidx.room:room-runtime:2.6.1", @@ -73,7 +92,10 @@ object app extends AndroidAppKotlinModule with AndroidBuildConfig with AndroidHi mvn"androidx.hilt:hilt-navigation-compose:1.2.0", mvn"com.google.accompanist:accompanist-swiperefresh:0.36.0", mvn"androidx.customview:customview-poolingcontainer:1.0.0", - mvn"androidx.tracing:tracing:1.2.0" + mvn"androidx.tracing:tracing:1.2.0", + // debug + mvn"androidx.compose.ui:ui-tooling:1.7.6", + mvn"androidx.compose.ui:ui-test-manifest:1.7.6" ) def kotlinSymbolProcessors: T[Seq[Dep]] = Seq( @@ -110,11 +132,63 @@ object app extends AndroidAppKotlinModule with AndroidBuildConfig with AndroidHi ) } - // TODO support instrumented tests on Hilt setups - object androidTest extends AndroidAppKotlinInstrumentedTests - with AndroidTestModule.AndroidJUnit with AndroidHiltSupport { + object androidTest extends AndroidAppKotlinInstrumentedTests with AndroidR8AppModule + with AndroidHiltSupport { + + override def kotlinLanguageVersion = Versions.kotlinLanguageVersion + + def moduleDeps = super.moduleDeps ++ Seq(`shared-test`) + + def testFramework = "com.example.android.architecture.blueprints.todoapp.CustomTestRunner" + + def androidEnableCompose = true + override def kspVersion = Versions.kspVersion + override def androidDebugSettings: T[AndroidBuildTypeSettings] = Task { + AndroidBuildTypeSettings( + isMinifyEnabled = false, + isShrinkEnabled = false + ).withDefaultProguardFile("proguard-android.txt") + .withProguardLocalFiles( + Seq( + moduleDir / "proguardTest-rules.pro", + moduleDir / "proguard-rules.pro" + ) + ) + } + + def mvnDeps = super.mvnDeps() ++ Seq( + // Dependencies for Android unit tests + mvn"androidx.compose:compose-bom:2024.12.01", + 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", + mvn"androidx.compose.ui:ui-test-junit4:1.7.6", + // AndroidX Test - Instrumented testing + mvn"androidx.test:core-ktx:1.6.1", + mvn"androidx.test.ext:junit-ktx:1.2.1", + mvn"androidx.test:rules:1.6.1", + mvn"androidx.room:room-testing:2.6.1", + mvn"androidx.arch.core:core-testing:2.2.0", + mvn"androidx.navigation:navigation-testing:2.8.5", + mvn"androidx.test.espresso:espresso-core:3.6.1", + mvn"androidx.test.espresso:espresso-contrib:3.6.1", + mvn"androidx.test.espresso:espresso-intents:3.6.1", + mvn"androidx.test.espresso:espresso-idling-resource:3.6.1", + mvn"androidx.test.espresso.idling:idling-concurrent:3.6.1", + // AndroidX Test - Hilt testing + mvn"com.google.dagger:hilt-android-testing:2.56", + // debug + mvn"androidx.compose.ui:ui-tooling:1.7.6", + mvn"androidx.compose.ui:ui-test-manifest:1.7.6" + ) + + def kotlinSymbolProcessors: T[Seq[Dep]] = Seq( + mvn"androidx.room:room-compiler:2.6.1", + mvn"com.google.dagger:hilt-android-compiler:2.56" + ) + } } @@ -186,6 +260,13 @@ object `shared-test` extends AndroidKotlinModule with AndroidHiltSupport { "Complete" ] +> ./mill app.androidTest + +> cat out/app/androidTest/testForked.dest/test-report.xml + + +... + > ./mill show app.stopAndroidEmulator > ./mill show app.deleteAndroidVirtualDevice diff --git a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala index b2e54eeb8d5..f9324d8d42f 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala @@ -85,6 +85,10 @@ trait AndroidAppModule extends AndroidModule { outer => } + def androidDebugManifestLocation: T[PathRef] = Task.Source { + "src/debug/AndroidManifest.xml" + } + /** * Provides os.Path to an XML file containing configuration and metadata about your android application. * TODO dynamically add android:debuggable @@ -214,6 +218,25 @@ trait AndroidAppModule extends AndroidModule { outer => def androidPackageableExtraFiles: T[Seq[AndroidPackageableExtraFile]] = Task { Seq.empty[AndroidPackageableExtraFile] } + def androidPackageMetaInfoFiles: T[Seq[AndroidPackageableExtraFile]] = Task { + def metaInfRoot(p: os.Path): os.Path = { + var current = p + while (!current.endsWith(os.rel / "META-INF")) { + current = current / os.up + } + current / os.up + } + + androidLibsClassesJarMetaInf() + .map(ref => + AndroidPackageableExtraFile( + PathRef(ref.path), + ref.path.subRelativeTo(metaInfRoot(ref.path)) + ) + ).distinctBy(_.destination) + + } + /** * Packages DEX files and Android resources into an unsigned APK. * @@ -226,20 +249,10 @@ trait AndroidAppModule extends AndroidModule { outer => val dexFiles = os.walk(androidDex().path) .filter(_.ext == "dex") .map(os.zip.ZipSource.fromPath) - // TODO probably need to merge all content, not only in META-INF of classes.jar, but also outside it - val metaInf = androidLibsClassesJarMetaInf() - .map(ref => { - def metaInfRoot(p: os.Path): os.Path = { - var current = p - while (!current.endsWith(os.rel / "META-INF")) { - current = current / os.up - } - current / os.up - } - val path = ref.path - os.zip.ZipSource.fromPathTuple((path, path.subRelativeTo(metaInfRoot(path)))) - }) - .distinctBy(_.dest.get) + + val metaInf = androidPackageMetaInfoFiles().map(extraFile => + os.zip.ZipSource.fromPathTuple((extraFile.source.path, extraFile.destination.asSubPath)) + ) // add all the extra files to the APK val extraFiles: Seq[zip.ZipSource] = androidPackageableExtraFiles().map(extraFile => @@ -271,7 +284,9 @@ trait AndroidAppModule extends AndroidModule { outer => * See [[https://developer.android.com/build/manage-manifests]] for more details. */ def androidMergedManifest: T[PathRef] = Task { - val libManifests = androidUnpackArchives().flatMap(_.manifest) + val debugManifest = Seq(androidDebugManifestLocation().path).filter(os.exists) + val libManifests = androidUnpackArchives().flatMap(_.manifest.map(_.path)) + val allManifests = debugManifest ++ libManifests val mergedManifestPath = Task.dest / "AndroidManifest.xml" // TODO put it to the dedicated worker if cost of classloading is too high Jvm.callProcess( @@ -294,7 +309,7 @@ trait AndroidAppModule extends AndroidModule { outer => s"applicationId=${androidApplicationId}", "--out", mergedManifestPath.toString() - ) ++ libManifests.flatMap(m => Seq("--libs", m.path.toString())), + ) ++ allManifests.flatMap(m => Seq("--libs", m.toString)), classPath = manifestMergerClasspath().map(_.path).toVector, stdin = os.Inherit, stdout = os.Inherit @@ -791,105 +806,15 @@ trait AndroidAppModule extends AndroidModule { outer => throw new Exception("Device failed to boot") } - def androidModuleGeneratedDexVariants: Task[AndroidModuleGeneratedDexVariants] = Task { - val androidDebugDex = Task.dest / "androidDebugDex.dest" - os.makeDir(androidDebugDex) - val androidReleaseDex = Task.dest / "androidReleaseDex.dest" - os.makeDir(androidReleaseDex) - val mainDexListOutput = Task.dest / "main-dex-list-output.txt" - - val proguardFileDebug = androidDebugDex / "proguard-rules.pro" - - val knownProguardRulesDebug = androidUnpackArchives() - // TODO need also collect rules from other modules, - // but Android lib module doesn't yet exist - .flatMap(_.proguardRules) - .map(p => os.read(p.path)) - .appendedAll(mainDexPlatformRules) - .appended(os.read(androidCompiledResources().mainDexRulesProFile.path)) - .mkString("\n") - os.write(proguardFileDebug, knownProguardRulesDebug) - - val proguardFileRelease = androidReleaseDex / "proguard-rules.pro" - - val knownProguardRulesRelease = androidUnpackArchives() - // TODO need also collect rules from other modules, - // but Android lib module doesn't yet exist - .flatMap(_.proguardRules) - .map(p => os.read(p.path)) - .appendedAll(mainDexPlatformRules) - .appended(os.read(androidCompiledResources().mainDexRulesProFile.path)) - .mkString("\n") - os.write(proguardFileRelease, knownProguardRulesRelease) - - AndroidModuleGeneratedDexVariants( - androidDebugDex = PathRef(androidDebugDex), - androidReleaseDex = PathRef(androidReleaseDex), - mainDexListOutput = PathRef(mainDexListOutput) - ) - } - - /** ProGuard/R8 rules configuration files for release target (user-provided and generated) */ - def androidProguardReleaseConfigs: T[Seq[PathRef]] = Task { - val proguardFilesFromReleaseSettings = androidReleaseSettings().proguardFiles - val androidProguardPath = androidSdkModule().androidProguardPath().path - val defaultProguardFile = proguardFilesFromReleaseSettings.defaultProguardFile.map { - pf => androidProguardPath / pf - } - val userProguardFiles = proguardFilesFromReleaseSettings.localFiles - mill.define.BuildCtx.withFilesystemCheckerDisabled { - (defaultProguardFile.toSeq ++ userProguardFiles).map(PathRef(_)) - } - } - - /** - * The default release settings with the following settings: - * - minifyEnabled=true - * - shrinkEnabled=true - * - proguardFiles=proguard-android-optimize.txt - * @return - */ - def androidReleaseSettings: T[AndroidBuildTypeSettings] = Task { - AndroidBuildTypeSettings( - isMinifyEnabled = true, - isShrinkEnabled = true, - proguardFiles = ProguardFiles( - defaultProguardFile = Some("proguard-android-optimize.txt") - ) - ) - } - - def androidDebugSettings: T[AndroidBuildTypeSettings] = Task { - AndroidBuildTypeSettings() - } - - /** - * Gives the android build type settings for debug or release. - * Controlled by [[androidIsDebug]] flag! - * @return - */ - def androidBuildSettings: T[AndroidBuildTypeSettings] = Task { - if (androidIsDebug()) - androidDebugSettings() - else - androidReleaseSettings() - } - /** - * Converts the generated JAR file into a DEX file using the `d8` or the r8 tool if minification is enabled + * Converts the generated JAR file into a DEX file using the `d8` * through the [[androidBuildSettings]]. * * @return os.Path to the Generated DEX File Directory */ def androidDex: T[PathRef] = Task { - val buildSettings: AndroidBuildTypeSettings = androidBuildSettings() - - val dex = - if (buildSettings.isMinifyEnabled) - androidR8Dex() - else - androidD8Dex() + val dex = androidD8Dex() Task.log.debug("Building dex with command: " + dex.dexCliArgs.mkString(" ")) @@ -950,103 +875,6 @@ trait AndroidAppModule extends AndroidModule { outer => PathRef(outPath) -> d8Args } - // uses the R8 tool to generate the dex (to shrink and obfuscate) - private def androidR8Dex: Task[(outPath: PathRef, dexCliArgs: Seq[String])] = Task { - val destDir = Task.dest / "minify" - os.makeDir.all(destDir) - - val outputPath = destDir - - Task.log.debug("outptuPath: " + outputPath) - - // Define diagnostic output file paths - val mappingOut = destDir / "mapping.txt" - val seedsOut = destDir / "seeds.txt" - val usageOut = destDir / "usage.txt" - val configOut = destDir / "configuration.txt" - destDir / "missing_rules.txt" - val baselineOutOpt = destDir / "baseline-profile-rewritten.txt" - destDir / "res" - - // Create an extra ProGuard config file that instructs R8 to print seeds and usage. - val extraRulesFile = destDir / "extra-rules.pro" - val extraRulesContent = - s"""-printseeds ${seedsOut.toString} - |-printusage ${usageOut.toString} - |""".stripMargin.trim - os.write.over(extraRulesFile, extraRulesContent) - - val classpathClassFiles: Seq[String] = androidPackagedClassfiles() - .filter(_.path.ext == "class") - .map(_.path.toString) - - val appCompiledFiles: Seq[String] = androidPackagedCompiledClasses() - .filter(_.path.ext == "class") - .map(_.path.toString) - - val allClassFiles = classpathClassFiles ++ appCompiledFiles - - val r8ArgsBuilder = Seq.newBuilder[String] - - r8ArgsBuilder += androidSdkModule().r8Exe().path.toString - - if (androidIsDebug()) - r8ArgsBuilder += "--debug" - else - r8ArgsBuilder += "--release" - - r8ArgsBuilder ++= Seq( - "--output", - outputPath.toString, - "--pg-map-output", - mappingOut.toString, - "--pg-conf-output", - configOut.toString - ) - - if (!androidBuildSettings().enableDesugaring) { - r8ArgsBuilder += "--no-desugaring" - } - - if (!androidBuildSettings().isMinifyEnabled) { - r8ArgsBuilder += "--no-minification" - } - - if (!androidBuildSettings().isShrinkEnabled) { - r8ArgsBuilder += "--no-tree-shaking" - } - - r8ArgsBuilder ++= Seq( - "--min-api", - androidMinSdk().toString, - "--dex" - ) - - // Baseline profile rewriting arguments, if a baseline profile is provided. - val baselineArgs = baselineProfile().map { bp => - Seq("--art-profile", bp.path.toString, baselineOutOpt.toString) - }.getOrElse(Seq.empty) - - r8ArgsBuilder ++= baselineArgs - - // Library arguments: pass each bootclasspath and any additional library classes as --lib. - val libArgs = libraryClassesPaths().flatMap(ref => Seq("--lib", ref.path.toString)) - - r8ArgsBuilder ++= libArgs - - // ProGuard configuration files: add our extra rules file and all provided config files. - val pgArgs = Seq("--pg-conf", extraRulesFile.toString) ++ - androidProguardReleaseConfigs().flatMap(cfg => Seq("--pg-conf", cfg.path.toString)) - - r8ArgsBuilder ++= pgArgs - - r8ArgsBuilder ++= allClassFiles - - val r8Args = r8ArgsBuilder.result() - - PathRef(outputPath) -> r8Args - } - trait AndroidAppTests extends AndroidAppModule with JavaTests { override def androidCompileSdk: T[Int] = outer.androidCompileSdk() @@ -1102,10 +930,22 @@ trait AndroidAppModule extends AndroidModule { outer => override def generatedSources: T[Seq[PathRef]] = Task.Sources() private def androidInstrumentedTestsBaseManifest: Task[Elem] = Task.Anon { - - {androidManifestUsesSdkSection()} + + + + + + } @@ -1115,20 +955,58 @@ trait AndroidAppModule extends AndroidModule { outer => * @return */ override def androidManifest: T[PathRef] = Task { - val baseManifestElem = androidInstrumentedTestsBaseManifest() - val testFrameworkName = testFramework() - val manifestWithInstrumentation = { - val instrumentation = - - baseManifestElem.copy(child = baseManifestElem.child ++ instrumentation) - } val destManifest = Task.dest / "AndroidManifest.xml" - os.write(destManifest, manifestWithInstrumentation.toString) + os.write(destManifest, androidInstrumentedTestsBaseManifest().toString) PathRef(destManifest) } + private def androidxTestManifests: Task[Seq[PathRef]] = Task { + androidUnpackArchives().flatMap { + unpackedArchive => + unpackedArchive.manifest.map(_.path) + }.filter { + case manifest: os.Path => + val manifestXML = XML.loadFile(manifest.toString) + (manifestXML \\ "manifest") + .map(_ \ "@package").map(_.text).exists(_.startsWith("androidx.test")) + }.map(PathRef(_)) + } + + /** + * Creates a merged manifest for instrumented tests. + * + * See [[https://developer.android.com/build/manage-manifests]] for more details. + */ + def androidMergedManifest: T[PathRef] = Task { + val debugManifest = Seq(outer.androidDebugManifestLocation().path).filter(os.exists) + val androidxManifests = androidxTestManifests().map(_.path) + val libManifests = (debugManifest ++ androidxManifests) + val mergedManifestPath = Task.dest / "AndroidManifest.xml" + // TODO put it to the dedicated worker if cost of classloading is too high + Jvm.callProcess( + mainClass = "com.android.manifmerger.Merger", + mainArgs = Seq( + "--main", + androidManifest().path.toString(), + "--remove-tools-declarations", + "--property", + s"min_sdk_version=${androidMinSdk()}", + "--property", + s"target_sdk_version=${androidTargetSdk()}", + "--property", + s"version_code=${androidVersionCode()}", + "--property", + s"target_package=${outer.androidApplicationId}", + "--property", + s"version_name=${androidVersionName()}", + "--out", + mergedManifestPath.toString() + ) ++ libManifests.flatMap(m => Seq("--libs", m.toString())), + classPath = manifestMergerClasspath().map(_.path) + ) + PathRef(mergedManifestPath) + } + override def androidVirtualDeviceIdentifier: String = outer.androidVirtualDeviceIdentifier override def androidEmulatorArchitecture: String = outer.androidEmulatorArchitecture @@ -1178,7 +1056,7 @@ trait AndroidAppModule extends AndroidModule { outer => "instrument", "-w", "-r", - s"${androidApplicationNamespace}/${testFramework()}" + s"${androidApplicationId}/${testFramework()}" ) ).spawn() @@ -1191,9 +1069,21 @@ trait AndroidAppModule extends AndroidModule { outer => } + /** + * The androidTestClasspath dictates what we are going to package + * in the test apk. This should have all moduleDeps except the main AndroidAppModule + * as its apk is installed separately + */ + def androidTransitiveTestClasspath: T[Seq[PathRef]] = Task { + Task.traverse(transitiveModuleCompileModuleDeps) { + m => + Task.Anon(m.localRunClasspath()) + }().flatten + } + /** The instrumented dex should just contain the test dependencies and locally tested files */ override def androidPackagedClassfiles: T[Seq[PathRef]] = Task { - testClasspath() + (testClasspath() ++ androidTransitiveTestClasspath()) .map(_.path).filter(os.isDir) .flatMap(os.walk(_)) .filter(os.isFile) @@ -1202,7 +1092,7 @@ trait AndroidAppModule extends AndroidModule { outer => } override def androidPackagedDeps: T[Seq[PathRef]] = Task { - androidResolvedRunMvnDeps() + androidResolvedMvnDeps() } /** diff --git a/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala b/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala index e770d74bedf..e6966cf51a0 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala @@ -13,7 +13,7 @@ import mill.define.JsonFormatters.pathReadWrite case class AndroidBuildTypeSettings( isMinifyEnabled: Boolean = false, isShrinkEnabled: Boolean = false, - enableDesugaring: Boolean = false, + enableDesugaring: Boolean = true, proguardFiles: ProguardFiles = ProguardFiles() ) { def withProguardLocalFiles(localFiles: Seq[os.Path]): AndroidBuildTypeSettings = diff --git a/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala new file mode 100644 index 00000000000..da49c25da4f --- /dev/null +++ b/libs/androidlib/src/mill/androidlib/AndroidR8AppModule.scala @@ -0,0 +1,223 @@ +package mill.androidlib + +import mill.* +import mill.define.{PathRef, Task} + +@mill.api.experimental +trait AndroidR8AppModule extends AndroidAppModule { + + override def androidPackageMetaInfoFiles: T[Seq[AndroidPackageableExtraFile]] = + androidR8PackageMetaInfoFiles() + + /** + * Converts the generated JAR file into a DEX file using the r8 tool if minification is enabled + * through the [[androidBuildSettings]]. + * + * @return os.Path to the Generated DEX File Directory + */ + def androidDex: T[PathRef] = Task { + + val dex = androidR8Dex() + + Task.log.debug("Building dex with command: " + dex.dexCliArgs.mkString(" ")) + + os.call(dex.dexCliArgs) + + dex.outPath + + } + + /** + * Selects the meta info and metadata files to package. These are being extracted + * and output by R8 from the dependency jars. + * + * @return A list of files to package into the apk + */ + def androidR8PackageMetaInfoFiles: T[Seq[AndroidPackageableExtraFile]] = Task { + val root = androidDex().path + + def directoryFiles(dir: os.Path): Seq[os.Path] = if (os.exists(dir)) + os.walk(dir).filter(os.isFile) + else + Seq.empty[os.Path] + + val metaInfoFiles = directoryFiles(root / "META-INF") + + val kotlinMetadataFiles = directoryFiles(root / "kotlin") + + val includedFiles = (metaInfoFiles ++ kotlinMetadataFiles) + + includedFiles.map(nonDex => + AndroidPackageableExtraFile(PathRef(nonDex), nonDex.relativeTo(root)) + ) + } + + def androidLibraryProguardConfigs: Task[Seq[PathRef]] = Task { + androidUnpackArchives() + // TODO need also collect rules from other modules, + // but Android lib module doesn't yet exist + .flatMap(_.proguardRules) + } + + /** ProGuard/R8 rules configuration files for release target (user-provided and generated) */ + def androidProguardConfigs: Task[Seq[PathRef]] = Task { + val proguardFilesFromBuildSettings = androidBuildSettings().proguardFiles + val androidProguardPath = androidSdkModule().androidProguardPath().path + val defaultProguardFile = proguardFilesFromBuildSettings.defaultProguardFile.map { + pf => androidProguardPath / pf + } + val userProguardFiles = proguardFilesFromBuildSettings.localFiles + mill.define.BuildCtx.withFilesystemCheckerDisabled { + (defaultProguardFile.toSeq ++ userProguardFiles).map(PathRef( + _ + )) ++ androidLibraryProguardConfigs() + } + } + + /** Concatenates all rules into one file */ + def androidProguard: T[PathRef] = Task { + val globalProguard = Task.dest / "global-proguard.pro" + val files = androidProguardConfigs() + os.write(globalProguard, "") + files.foreach(pg => + os.write.append(globalProguard, os.read(pg.path)) + ) + PathRef(globalProguard) + } + + /** + * The default release settings with the following settings: + * - minifyEnabled=true + * - shrinkEnabled=true + * - proguardFiles=proguard-android-optimize.txt + * + * @return + */ + def androidReleaseSettings: T[AndroidBuildTypeSettings] = Task { + AndroidBuildTypeSettings( + isMinifyEnabled = true, + isShrinkEnabled = true, + proguardFiles = ProguardFiles( + defaultProguardFile = Some("proguard-android-optimize.txt") + ) + ) + } + + def androidDebugSettings: T[AndroidBuildTypeSettings] = Task { + AndroidBuildTypeSettings() + } + + /** + * Gives the android build type settings for debug or release. + * Controlled by [[androidIsDebug]] flag! + * + * @return + */ + def androidBuildSettings: T[AndroidBuildTypeSettings] = Task { + if (androidIsDebug()) + androidDebugSettings() + else + androidReleaseSettings() + } + + /** + * Prepares the R8 cli command to build this android app! + * @return + */ + def androidR8Dex: Task[(outPath: PathRef, dexCliArgs: Seq[String])] = Task { + val destDir = Task.dest / "minify" + os.makeDir.all(destDir) + + val outputPath = destDir + + Task.log.debug("outptuPath: " + outputPath) + + // Define diagnostic output file paths + val mappingOut = destDir / "mapping.txt" + val seedsOut = destDir / "seeds.txt" + val usageOut = destDir / "usage.txt" + val configOut = destDir / "configuration.txt" + destDir / "missing_rules.txt" + val baselineOutOpt = destDir / "baseline-profile-rewritten.txt" + destDir / "res" + + // Create an extra ProGuard config file that instructs R8 to print seeds and usage. + val extraRulesFile = destDir / "extra-rules.pro" + val extraRulesContent = + s"""-printseeds ${seedsOut.toString} + |-printusage ${usageOut.toString} + |""".stripMargin.trim + os.write.over(extraRulesFile, extraRulesContent) + + val classpathClassFiles: Seq[String] = androidPackagedClassfiles() + .filter(_.path.ext == "class") + .map(_.path.toString) + + val appCompiledFiles: Seq[String] = androidPackagedCompiledClasses() + .filter(_.path.ext == "class") + .map(_.path.toString) + + val allClassFiles = + classpathClassFiles ++ appCompiledFiles ++ androidPackagedDeps().map(_.path.toString) + + val r8ArgsBuilder = Seq.newBuilder[String] + + r8ArgsBuilder += androidSdkModule().r8Exe().path.toString + + if (androidIsDebug()) + r8ArgsBuilder += "--debug" + else + r8ArgsBuilder += "--release" + + r8ArgsBuilder ++= Seq( + "--output", + outputPath.toString, + "--pg-map-output", + mappingOut.toString, + "--pg-conf-output", + configOut.toString + ) + + if (!androidBuildSettings().enableDesugaring) { + r8ArgsBuilder += "--no-desugaring" + } + + if (!androidBuildSettings().isMinifyEnabled) { + r8ArgsBuilder += "--no-minification" + } + + if (!androidBuildSettings().isShrinkEnabled) { + r8ArgsBuilder += "--no-tree-shaking" + } + + r8ArgsBuilder ++= Seq( + "--min-api", + androidMinSdk().toString, + "--dex" + ) + + // Baseline profile rewriting arguments, if a baseline profile is provided. + val baselineArgs = baselineProfile().map { bp => + Seq("--art-profile", bp.path.toString, baselineOutOpt.toString) + }.getOrElse(Seq.empty) + + r8ArgsBuilder ++= baselineArgs + + // Library arguments: pass each bootclasspath and any additional library classes as --lib. + val libArgs = libraryClassesPaths().flatMap(ref => Seq("--lib", ref.path.toString)) + + r8ArgsBuilder ++= libArgs + + // ProGuard configuration files: add our extra rules file and all provided config files. + val pgArgs = Seq("--pg-conf", androidProguard().path.toString) + + r8ArgsBuilder ++= pgArgs + + r8ArgsBuilder ++= allClassFiles + + val r8Args = r8ArgsBuilder.result() + + PathRef(outputPath) -> r8Args + } + +} diff --git a/libs/androidlib/src/mill/androidlib/InstrumentationOutput.scala b/libs/androidlib/src/mill/androidlib/InstrumentationOutput.scala index 40d028945b6..74bd6b529a1 100644 --- a/libs/androidlib/src/mill/androidlib/InstrumentationOutput.scala +++ b/libs/androidlib/src/mill/androidlib/InstrumentationOutput.scala @@ -89,7 +89,7 @@ private[androidlib] object InstrumentationOutput { TimeResultState(state.started, testResultStarted, state.testResults :+ testResult) case InstrumentationOutput.Ignored(line) => // todo handle stream and stack - logger.debug(s"Message ${line}, ignored") + logger.debug(s"Message ${line}") state } } diff --git a/libs/androidlib/src/mill/androidlib/hilt/AndroidHiltSupport.scala b/libs/androidlib/src/mill/androidlib/hilt/AndroidHiltSupport.scala index afacaa4ee04..ffef898e8c4 100644 --- a/libs/androidlib/src/mill/androidlib/hilt/AndroidHiltSupport.scala +++ b/libs/androidlib/src/mill/androidlib/hilt/AndroidHiltSupport.scala @@ -21,7 +21,7 @@ import mill.{T, Task} trait AndroidHiltSupport extends KspModule with AndroidKotlinModule { override def kspClasspath: T[Seq[PathRef]] = - Seq(androidProcessedResources()) ++ super.kspClasspath() + super.kspClasspath() def androidHiltProcessorPath: T[Seq[PathRef]] = Task { defaultResolver().classpath(