diff --git a/example/androidlib/java/1-hello-world/app/src/main/AndroidManifest.xml b/example/androidlib/java/1-hello-world/app/src/main/AndroidManifest.xml index c7d5ad74648d..9f7a4b9ad4cd 100644 --- a/example/androidlib/java/1-hello-world/app/src/main/AndroidManifest.xml +++ b/example/androidlib/java/1-hello-world/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - @@ -9,7 +8,4 @@ - \ No newline at end of file diff --git a/example/androidlib/java/1-hello-world/build.mill b/example/androidlib/java/1-hello-world/build.mill index 4343b559aeb1..08ecc2a4016d 100644 --- a/example/androidlib/java/1-hello-world/build.mill +++ b/example/androidlib/java/1-hello-world/build.mill @@ -45,8 +45,6 @@ object app extends AndroidAppModule { def androidSdkModule = mill.define.ModuleRef(androidSdkModule0) - override def instrumentationPackage = "com.helloworld.app" - /* 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 @@ -141,7 +139,7 @@ object app extends AndroidAppModule { ] ... -> cat out/app/it/testTask.dest/test-report.xml +> cat out/app/it/testForked.dest/test-report.xml ... diff --git a/example/androidlib/java/2-app-bundle/bundle/src/main/AndroidManifest.xml b/example/androidlib/java/2-app-bundle/bundle/src/main/AndroidManifest.xml index c17c9339b78e..9f7a4b9ad4cd 100644 --- a/example/androidlib/java/2-app-bundle/bundle/src/main/AndroidManifest.xml +++ b/example/androidlib/java/2-app-bundle/bundle/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - diff --git a/example/androidlib/java/3-linting/app/src/main/AndroidManifest.xml b/example/androidlib/java/3-linting/app/src/main/AndroidManifest.xml index 274041a38963..43eadc35b9e0 100644 --- a/example/androidlib/java/3-linting/app/src/main/AndroidManifest.xml +++ b/example/androidlib/java/3-linting/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - diff --git a/example/androidlib/java/3-linting/build.mill b/example/androidlib/java/3-linting/build.mill index 55065c28f731..5befed6557c5 100644 --- a/example/androidlib/java/3-linting/build.mill +++ b/example/androidlib/java/3-linting/build.mill @@ -67,7 +67,7 @@ lint.xml src > cat out/app/androidLintRun.dest/report.txt # Display content of the linting report -AndroidManifest.xml:3: ...Error: Avoid hardcoding the debug mode; leaving it out allows debug and release builds to automatically assign one [HardcodedDebugMode] +AndroidManifest.xml:2: ...Error: Avoid hardcoding the debug mode; leaving it out allows debug and release builds to automatically assign one [HardcodedDebugMode] > sed -i.bak 's/ android:debuggable="true"//g' app/src/main/AndroidManifest.xml # Fix the HardcodedDebugMode warning issue from `AndroidManifest.xml` @@ -91,7 +91,7 @@ AndroidManifest.xml:3: ...Error: Avoid hardcoding the debug mode; leaving it out > ./mill app.androidLintRun # Rerun it for new changes to reflect > cat out/app/androidLintRun.dest/report.txt # Output the changes in the report -AndroidManifest.xml:3: ...Warning: Should explicitly set android:icon, there is no default [MissingApplicationIcon] +AndroidManifest.xml:2: ...Warning: Should explicitly set android:icon, there is no default [MissingApplicationIcon] > sed -i.bak 's/severity="warning"/severity="ignore"/g' app/lint.xml # Revert the severity level of `MissingApplicationIcon` diff --git a/example/androidlib/java/4-sum-lib-java/app/src/main/AndroidManifest.xml b/example/androidlib/java/4-sum-lib-java/app/src/main/AndroidManifest.xml index b200b9cc52b6..b63ad440259d 100644 --- a/example/androidlib/java/4-sum-lib-java/app/src/main/AndroidManifest.xml +++ b/example/androidlib/java/4-sum-lib-java/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - diff --git a/example/androidlib/java/4-sum-lib-java/build.mill b/example/androidlib/java/4-sum-lib-java/build.mill index 1cf994da3bc8..49377b03c8bc 100644 --- a/example/androidlib/java/4-sum-lib-java/build.mill +++ b/example/androidlib/java/4-sum-lib-java/build.mill @@ -28,6 +28,8 @@ object lib extends AndroidLibModule with PublishModule { def androidMinSdk = 19 def androidCompileSdk = 35 + def androidLibPackage = "com.example" + def publishVersion = "0.0.1" def pomSettings = PomSettings( description = "sumlib", diff --git a/example/androidlib/java/4-sum-lib-java/lib/src/main/AndroidManifest.xml b/example/androidlib/java/4-sum-lib-java/lib/src/main/AndroidManifest.xml index 1b1b9af6f049..25f71c666845 100644 --- a/example/androidlib/java/4-sum-lib-java/lib/src/main/AndroidManifest.xml +++ b/example/androidlib/java/4-sum-lib-java/lib/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/example/androidlib/java/5-R8/app/src/main/AndroidManifest.xml b/example/androidlib/java/5-R8/app/src/main/AndroidManifest.xml index 0e5cf38cd85e..9f7a4b9ad4cd 100644 --- a/example/androidlib/java/5-R8/app/src/main/AndroidManifest.xml +++ b/example/androidlib/java/5-R8/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - + @@ -9,7 +8,4 @@ - \ No newline at end of file diff --git a/example/androidlib/java/5-R8/app/test-proguard-rules.pro b/example/androidlib/java/5-R8/app/test-proguard-rules.pro index 304965c885de..527b6524e794 100644 --- a/example/androidlib/java/5-R8/app/test-proguard-rules.pro +++ b/example/androidlib/java/5-R8/app/test-proguard-rules.pro @@ -9,6 +9,7 @@ # Keep all annotation metadata (needed for reflection-based test frameworks) -keepattributes *Annotation* +-keep class com.helloworld.app.** { *; } # Keep all Espresso framework classes and specifically ensure that the idling resources aren’t stripped -keep class androidx.test.espresso.** { *; } -keep class androidx.test.espresso.IdlingRegistry { *; } diff --git a/example/androidlib/java/5-R8/build.mill b/example/androidlib/java/5-R8/build.mill index 142d647aa20b..120d100041b6 100644 --- a/example/androidlib/java/5-R8/build.mill +++ b/example/androidlib/java/5-R8/build.mill @@ -43,6 +43,15 @@ object app extends AndroidAppModule { // <2> def androidReleaseKeyStorePass: T[Option[String]] = Task { Some("MillBuildTool") } override def androidVirtualDeviceIdentifier: String = "java-test" + override def androidIsDebug: T[Boolean] = Task { false } + + override def androidReleaseSettings: T[AndroidBuildTypeSettings] = Task { + super.androidReleaseSettings().withProguardLocalFiles( + Seq( + moduleDir / "proguard-rules.pro" + ) + ) + } // Unit tests for the application object test extends AndroidAppTests with TestModule.Junit4 { @@ -54,7 +63,14 @@ object app extends AndroidAppModule { // <2> // Instrumented tests (runs on emulator) object it extends AndroidAppInstrumentedTests with AndroidTestModule.AndroidJUnit { def androidSdkModule = mill.define.ModuleRef(androidSdkModule0) - override def instrumentationPackage = "com.helloworld.app" + + override def androidIsDebug: T[Boolean] = Task { + 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 @@ -80,25 +96,39 @@ object app extends AndroidAppModule { // <2> /** Usage > ./mill show app.androidApk -".../out/app/androidApk.dest/app.apk" > ./mill show app.createAndroidVirtualDevice ...Name: java-test, DeviceId: medium_phone... > ./mill show app.startAndroidEmulator -> ./mill show app.androidReleaseInstall +> ./mill show app.androidInstall ...All files should be loaded. Notifying the device... +> ./mill show app.it +... +[ + "", + [ + { + "fullyQualifiedName": "com.helloworld.app.ExampleInstrumentedTest.useAppContext", + "selector": "com.helloworld.app.ExampleInstrumentedTest.useAppContext", + "duration": ..., + "status": "Success" + } + ] +] + + > ./mill show app.stopAndroidEmulator > ./mill show app.deleteAndroidVirtualDevice */ -// R8 will run automatically when you run the `androidReleaseInstall` task. -// If you want to create the APK without R8, you can use the `androidApk` task to create the not-optimized APK. You can also +// R8 will run automatically when you run the `androidInstall` task with androidIsDebug set to false. +// If you want to create the APK without R8, you can set the androidReleaseSettings isMinifyEnabled to false. You can also // run the `andoidInstall` task that will automaticaly run the `androidApk` and also install it in the emulator. -// The `androidReleaseInstall` task will install the optimized APK on the emulator. +// The release settings used in `androidInstall` task will install the optimized APK on the emulator. // So first you need to create the emulator and start it. // After the emulator is started, you can run the `androidReleaseInstall` task and see the app in the emulator. diff --git a/example/androidlib/kotlin/1-hello-kotlin/app/proguard-rules.pro b/example/androidlib/kotlin/1-hello-kotlin/app/proguard-rules.pro index 5be28d0e1a03..6bccbb97330c 100644 --- a/example/androidlib/kotlin/1-hello-kotlin/app/proguard-rules.pro +++ b/example/androidlib/kotlin/1-hello-kotlin/app/proguard-rules.pro @@ -13,12 +13,10 @@ # public *; #} -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +# Suppress warnings +-ignorewarnings -# If you keep the line number information, uncomment this to -# hide the original source file name. --renamesourcefileattribute SourceFilex +# Keep all annotation metadata (needed for reflection-based test frameworks) +-keepattributes *Annotation* -keep class com.helloworld.** { *; } \ No newline at end of file diff --git a/example/androidlib/kotlin/1-hello-kotlin/app/src/main/AndroidManifest.xml b/example/androidlib/kotlin/1-hello-kotlin/app/src/main/AndroidManifest.xml index d93582cd46bc..9f7a4b9ad4cd 100644 --- a/example/androidlib/kotlin/1-hello-kotlin/app/src/main/AndroidManifest.xml +++ b/example/androidlib/kotlin/1-hello-kotlin/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - @@ -9,8 +8,4 @@ - - \ No newline at end of file diff --git a/example/androidlib/kotlin/1-hello-kotlin/app/test-proguard-rules.pro b/example/androidlib/kotlin/1-hello-kotlin/app/test-proguard-rules.pro index 304965c885de..2b6077557422 100644 --- a/example/androidlib/kotlin/1-hello-kotlin/app/test-proguard-rules.pro +++ b/example/androidlib/kotlin/1-hello-kotlin/app/test-proguard-rules.pro @@ -11,6 +11,7 @@ # Keep all Espresso framework classes and specifically ensure that the idling resources aren’t stripped -keep class androidx.test.espresso.** { *; } +-keep class androidx.test.** { *; } -keep class androidx.test.espresso.IdlingRegistry { *; } -keep class androidx.test.espresso.IdlingResource { *; } diff --git a/example/androidlib/kotlin/1-hello-kotlin/build.mill b/example/androidlib/kotlin/1-hello-kotlin/build.mill index 5e08addfb0ce..6f56fca25320 100644 --- a/example/androidlib/kotlin/1-hello-kotlin/build.mill +++ b/example/androidlib/kotlin/1-hello-kotlin/build.mill @@ -41,13 +41,36 @@ object app extends AndroidAppKotlinModule { override def androidVirtualDeviceIdentifier: String = "kotlin-test" override def androidEmulatorPort: String = "5556" + 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" } object it extends AndroidAppKotlinInstrumentedTests with AndroidTestModule.AndroidJUnit { - override def instrumentationPackage = "com.helloworld.app" + // TODO currently instrumented tests debug mode + // is coupled with the app debug mode. Fix so + // that instrumented tests can be built with debug + // configuration but the apk signature will match + // the app apk + override def androidIsDebug: T[Boolean] = Task { + 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 @@ -75,18 +98,6 @@ object app extends AndroidAppKotlinModule { > ./mill show app.androidApk ".../out/app/androidApk.dest/app.apk" -> ./mill show app.createAndroidVirtualDevice -...Name: kotlin-test, DeviceId: medium_phone... - -> ./mill show app.startAndroidEmulator - -> ./mill show app.androidReleaseInstall -...All files should be loaded. Notifying the device... - -> ./mill show app.stopAndroidEmulator - -> ./mill show app.deleteAndroidVirtualDevice - */ // This command triggers the build process, which installs the Android Setup, compiles the kotlin @@ -156,7 +167,7 @@ object app extends AndroidAppKotlinModule { ] ... -> cat out/app/it/testTask.dest/test-report.xml +> cat out/app/it/testForked.dest/test-report.xml ... diff --git a/example/androidlib/kotlin/3-compose-screenshot-tests/app/src/main/AndroidManifest.xml b/example/androidlib/kotlin/3-compose-screenshot-tests/app/src/main/AndroidManifest.xml index 102283be0487..825d624faa03 100644 --- a/example/androidlib/kotlin/3-compose-screenshot-tests/app/src/main/AndroidManifest.xml +++ b/example/androidlib/kotlin/3-compose-screenshot-tests/app/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ - + - diff --git a/example/androidlib/kotlin/4-sum-lib-kotlin/build.mill b/example/androidlib/kotlin/4-sum-lib-kotlin/build.mill index 7228fae60177..a14c11cdd865 100644 --- a/example/androidlib/kotlin/4-sum-lib-kotlin/build.mill +++ b/example/androidlib/kotlin/4-sum-lib-kotlin/build.mill @@ -28,6 +28,8 @@ object lib extends AndroidLibKotlinModule with PublishModule { def androidCompileSdk = 35 def kotlinVersion = "2.0.20" + def androidLibPackage = "com.example" + def publishVersion = "0.0.1" def pomSettings = PomSettings( description = "sumlib", diff --git a/example/androidlib/kotlin/4-sum-lib-kotlin/lib/src/main/AndroidManifest.xml b/example/androidlib/kotlin/4-sum-lib-kotlin/lib/src/main/AndroidManifest.xml index 1b1b9af6f049..25f71c666845 100644 --- a/example/androidlib/kotlin/4-sum-lib-kotlin/lib/src/main/AndroidManifest.xml +++ b/example/androidlib/kotlin/4-sum-lib-kotlin/lib/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/example/thirdparty/androidtodo/build.mill b/example/thirdparty/androidtodo/build.mill index 28b1ca371c46..0c7ff5fe2db9 100644 --- a/example/thirdparty/androidtodo/build.mill +++ b/example/thirdparty/androidtodo/build.mill @@ -111,10 +111,9 @@ object app extends AndroidAppKotlinModule with AndroidBuildConfig with AndroidHi ) } - object androidTest extends AndroidAppKotlinInstrumentedTests with AndroidTestModule.AndroidJUnit { - override def instrumentationPackage = "com.example.android" - - } + // TODO support instrumented tests on Hilt setups + object androidTest extends AndroidAppKotlinInstrumentedTests + with AndroidTestModule.AndroidJUnit {} } diff --git a/libs/androidlib/src/mill/androidlib/AndroidAppKotlinModule.scala b/libs/androidlib/src/mill/androidlib/AndroidAppKotlinModule.scala index 80eb7a8ff804..aec98a0b4ab7 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidAppKotlinModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidAppKotlinModule.scala @@ -66,8 +66,6 @@ trait AndroidAppKotlinModule extends AndroidAppModule with AndroidKotlinModule { override def androidCompileSdk: T[Int] = outer.androidCompileSdk() - override def androidMergedManifest: T[PathRef] = outer.androidMergedManifest() - override def androidSdkModule: ModuleRef[AndroidSdkModule] = outer.androidSdkModule // FIXME: avoid hardcoded version diff --git a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala index 0477a9b57f3f..e0ae83c99c79 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidAppModule.scala @@ -11,7 +11,7 @@ import os.RelPath import upickle.default.* import scala.jdk.OptionConverters.RichOptional -import scala.xml.{Attribute, Null, Text, XML} +import scala.xml.{Attribute, Elem, NodeBuffer, Null, Text, XML} /** * Enumeration for Android Lint report formats, providing predefined formats @@ -51,9 +51,11 @@ object AndroidLintReportFormat extends Enumeration { * [[https://developer.android.com/studio Android Studio Documentation]] */ @mill.api.experimental -trait AndroidAppModule extends AndroidModule { +trait AndroidAppModule extends AndroidModule { outer => - private val parent: AndroidAppModule = this + protected val debugKeyStorePass = "mill-android" + protected val debugKeyAlias = "mill-android" + protected val debugKeyPass = "mill-android" /** * The namespace of the android application which is used @@ -63,6 +65,12 @@ trait AndroidAppModule extends AndroidModule { */ def androidApplicationNamespace: String + /** + * In the case of android apps this the [[androidApplicationNamespace]]. + * @return + */ + protected override def androidGeneratedResourcesPackage: String = androidApplicationNamespace + /** * Android Application Id which is typically package.main . * Can be used for build variants. @@ -71,8 +79,15 @@ trait AndroidAppModule extends AndroidModule { */ def androidApplicationId: String + private def androidManifestUsesSdkSection: Task[Elem] = Task.Anon { + val minSdkVersion = androidMinSdk().toString + val targetSdkVersion = androidTargetSdk().toString + + } + /** * Provides os.Path to an XML file containing configuration and metadata about your android application. + * TODO dynamically add android:debuggable */ override def androidManifest: T[PathRef] = Task { val manifestFromSourcePath = moduleDir / "src/main/AndroidManifest.xml" @@ -80,9 +95,14 @@ trait AndroidAppModule extends AndroidModule { val manifestElem = XML.loadFile(manifestFromSourcePath.toString()) // add the application package val manifestWithPackage = - manifestElem % Attribute(None, "package", Text(androidApplicationNamespace), Null) + manifestElem % Attribute(None, "package", Text(androidApplicationId), Null) + + val manifestWithUsesSdk = manifestWithPackage.copy( + child = androidManifestUsesSdkSection() ++ manifestWithPackage.child + ) + val generatedManifestPath = Task.dest / "AndroidManifest.xml" - os.write(generatedManifestPath, manifestWithPackage.mkString) + os.write(generatedManifestPath, manifestWithUsesSdk.mkString) PathRef(generatedManifestPath) } @@ -168,11 +188,6 @@ trait AndroidAppModule extends AndroidModule { ).distinct.map(PathRef(_)) } - /** - * Specifies AAPT options for Android resource compilation. - */ - def androidAaptOptions: T[Seq[String]] = Task { Seq("--auto-add-overlay") } - def androidTransitiveResources: Target[Seq[PathRef]] = Task { Task.traverse(transitiveModuleCompileModuleDeps) { m => Task.Anon(m.resources()) @@ -195,60 +210,30 @@ trait AndroidAppModule extends AndroidModule { } /** - * Converts the generated JAR file into a DEX file using the `d8` tool. - * - * @return os.Path to the Generated DEX File Directory + * Collect files from META-INF folder of classes.jar (not META-INF of aar in case of Android library). */ - override def androidDex: T[PathRef] = Task { - - val appCompiledFiles = os.walk(compile().classes.path) - .filter(_.ext == "class") - .map(_.toString) ++ inheritedClassFiles().map(_.path.toString()) - - val libsJarFiles = compileClasspath() - .filter(_ != androidSdkModule().androidJarPath()) - .filter(_.path.ext == "jar") - .map(_.path.toString()) - - val proguardFile = Task.dest / "proguard-rules.pro" - val knownProguardRules = 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(androidResources()._1.path / "main-dex-rules.pro")) - .mkString("\n") - os.write(proguardFile, knownProguardRules) - - val d8ArgsBuilder = Seq.newBuilder[String] - - d8ArgsBuilder += androidSdkModule().d8Path().path.toString - - if (androidIsDebug()) { - d8ArgsBuilder += "--debug" - } else { - d8ArgsBuilder += "--release" - } - // TODO explore --incremental flag for incremental builds - d8ArgsBuilder ++= Seq( - "--output", - Task.dest.toString(), - "--lib", - androidSdkModule().androidJarPath().path.toString(), - "--min-api", - androidMinSdk().toString, - "--main-dex-rules", - proguardFile.toString() - ) ++ appCompiledFiles ++ libsJarFiles - - val d8Args = d8ArgsBuilder.result() - - Task.log.info(s"Running d8 with the command: ${d8Args.mkString(" ")}") - - os.call(d8Args) - - PathRef(Task.dest) + def androidLibsClassesJarMetaInf: T[Seq[PathRef]] = Task { + // ^ not the best name for the method, but this is to distinguish between META-INF of aar and META-INF + // of classes.jar included in aar + compileClasspath() + .filter(ref => + ref.path.ext == "jar" && + ref != androidSdkModule().androidJarPath() + ) + .flatMap(ref => { + val dest = Task.dest / ref.path.baseName + os.unzip(ref.path, dest) + val lookupPath = dest / "META-INF" + if (os.exists(lookupPath)) { + os.walk(lookupPath) + .filter(os.isFile) + .filterNot(f => isExcludedFromPackaging(f.relativeTo(lookupPath))) + } else { + Seq.empty[os.Path] + } + }) + .map(PathRef(_)) + .toSeq } /** @@ -341,33 +326,6 @@ trait AndroidAppModule extends AndroidModule { ) } - /** - * Collect files from META-INF folder of classes.jar (not META-INF of aar in case of Android library). - */ - def androidLibsClassesJarMetaInf: T[Seq[PathRef]] = Task { - // ^ not the best name for the method, but this is to distinguish between META-INF of aar and META-INF - // of classes.jar included in aar - compileClasspath() - .filter(ref => - ref.path.ext == "jar" && - ref != androidSdkModule().androidJarPath() - ) - .flatMap(ref => { - val dest = Task.dest / ref.path.baseName - os.unzip(ref.path, dest) - val lookupPath = dest / "META-INF" - if (os.exists(lookupPath)) { - os.walk(lookupPath) - .filter(os.isFile) - .filterNot(f => isExcludedFromPackaging(f.relativeTo(lookupPath))) - } else { - Seq.empty[os.Path] - } - }) - .map(PathRef(_)) - .toSeq - } - /** * Optimizes the APK using the `zipalign` tool for better performance. * @@ -389,6 +347,38 @@ trait AndroidAppModule extends AndroidModule { PathRef(alignedApk) } + // TODO alias, keystore pass and pass below are sensitive credentials and shouldn't be leaked to disk/console. + // In the current state they are leaked, because Task dumps output to the json. + // Should be fixed ASAP. + + /** + * Name of the key alias in the release keystore. Default is not set. + */ + def androidReleaseKeyAlias: T[Option[String]] = Task { + None + } + + /** + * Name of the release keystore file. Default is not set. + */ + def androidReleaseKeyName: T[Option[String]] = Task { + None + } + + /** + * Password for the release key. Default is not set. + */ + def androidReleaseKeyPass: T[Option[String]] = Task { + None + } + + /** + * Password for the release keystore. Default is not set. + */ + def androidReleaseKeyStorePass: T[Option[String]] = Task { + None + } + /** * Generates the command-line arguments required for Android app signing. * @@ -568,7 +558,7 @@ trait AndroidAppModule extends AndroidModule { } /** - * Deletes the android device + * Deletes the android device */ def deleteAndroidVirtualDevice: T[os.CommandResult] = Task { os.call(( @@ -660,6 +650,10 @@ trait AndroidAppModule extends AndroidModule { * @return The name of the device the app was installed to */ def androidInstall(): Command[String] = Task.Command(exclusive = true) { + androidInstallTask() + } + + def androidInstallTask = Task.Anon { val emulator = runningEmulator() os.call( @@ -669,6 +663,19 @@ trait AndroidAppModule extends AndroidModule { emulator } + /** + * Default os.Path to the keystore file, derived from `androidReleaseKeyName()`. + * Users can customize the keystore file name to change this path. + */ + def androidReleaseKeyPath: T[Option[PathRef]] = Task { + androidReleaseKeyName().map(name => PathRef(moduleDir / name)) + } + + /* + The debug keystore is stored in `$HOME/.mill-android`. The practical + purpose of a global keystore is to avoid the user having to uninstall the + app everytime the task directory is deleted (as the app signatures will not match). + */ private def androidDebugKeystore: Task[PathRef] = Task(persistent = true) { val debugFileName = "mill-debug.jks" val globalDebugFileLocation = os.home / ".mill-android" @@ -703,11 +710,7 @@ trait AndroidAppModule extends AndroidModule { )) } - val debugKeystoreTaskFile = Task.dest / debugFileName - - os.copy(debugKeystoreFile, debugKeystoreTaskFile) - - PathRef(debugKeystoreTaskFile) + PathRef(debugKeystoreFile) } protected def androidKeystore: T[PathRef] = Task { @@ -719,6 +722,7 @@ trait AndroidAppModule extends AndroidModule { pathRef } + // TODO consider managing with proguard and/or r8 private def isExcludedFromPackaging(relPath: RelPath): Boolean = { val topPath = relPath.segments.head // TODO do this better @@ -778,9 +782,176 @@ trait AndroidAppModule extends AndroidModule { throw new Exception("Device failed to boot") } - def runR8: T[PathRef] = Task { + def androidModuleGeneratedDexVariants: Task[AndroidModuleGeneratedDexVariants] = Task { + val androidDebugDex = T.dest / "androidDebugDex.dest" + os.makeDir(androidDebugDex) + val androidReleaseDex = T.dest / "androidReleaseDex.dest" + os.makeDir(androidReleaseDex) + val mainDexListOutput = T.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(androidResources()._1.path / "main-dex-rules.pro")) + .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(androidResources()._1.path / "main-dex-rules.pro")) + .mkString("\n") + os.write(proguardFileRelease, knownProguardRulesRelease) + + AndroidModuleGeneratedDexVariants( + androidDebugDex = PathRef(androidDebugDex), + androidReleaseDex = PathRef(androidReleaseDex), + mainDexListOutput = PathRef(mainDexListOutput) + ) + } + + /** + * Provides the output path for the generated main-dex list file, which is used + * during the DEX generation process. + */ + def mainDexListOutput: T[Option[PathRef]] = Task { + Some(androidModuleGeneratedDexVariants().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 + + (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 + * through the [[androidBuildSettings]]. + * + * @return os.Path to the Generated DEX File Directory + */ + def androidDex: T[PathRef] = Task { + + val buildSettings: AndroidBuildTypeSettings = androidBuildSettings() + + val (outPath, dexCliArgs) = { + if (buildSettings.isMinifyEnabled) { + androidR8Dex() + } else + androidD8Dex() + } + + Task.log.debug("Building dex with command: " + dexCliArgs.mkString(" ")) + + os.call(dexCliArgs) + + outPath + + } + + // uses the d8 tool to generate the dex file, when minification is disabled + private def androidD8Dex: T[(PathRef, Seq[String])] = Task { + + val outPath = T.dest + + val appCompiledFiles = (androidPackagedCompiledClasses() ++ androidPackagedClassfiles()) + .map(_.path.toString()) + + val libsJarFiles = androidPackagedDeps() + .filter(_ != androidSdkModule().androidJarPath()) + .map(_.path.toString()) + + val proguardFile = Task.dest / "proguard-rules.pro" + val knownProguardRules = 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(androidResources()._1.path / "main-dex-rules.pro")) + .mkString("\n") + os.write(proguardFile, knownProguardRules) + + val d8ArgsBuilder = Seq.newBuilder[String] + + d8ArgsBuilder += androidSdkModule().d8Path().path.toString - val destDir = Task.dest / "minify" + if (androidIsDebug()) { + d8ArgsBuilder += "--debug" + } else { + d8ArgsBuilder += "--release" + } + // TODO explore --incremental flag for incremental builds + d8ArgsBuilder ++= Seq( + "--output", + outPath.toString(), + "--lib", + androidSdkModule().androidJarPath().path.toString(), + "--min-api", + androidMinSdk().toString, + "--main-dex-rules", + proguardFile.toString() + ) ++ appCompiledFiles ++ libsJarFiles + + val d8Args = d8ArgsBuilder.result() + + Task.log.info(s"Running d8 with the command: ${d8Args.mkString(" ")}") + + PathRef(outPath) -> d8Args + } + + // uses the R8 tool to generate the dex (to shrink and obfuscate) + private def androidR8Dex: Task[(PathRef, Seq[String])] = Task { + val destDir = T.dest / "minify" os.makeDir.all(destDir) val outputPath = destDir @@ -804,18 +975,15 @@ trait AndroidAppModule extends AndroidModule { |""".stripMargin.trim os.write.over(extraRulesFile, extraRulesContent) - // Get the list of all class files to be processed by R8 - super.compileClasspath().map(_.path).filter(os.isDir) - .flatMap(os.walk(_)) - .filter(os.isFile) - .filter(_.ext == "class") - .map(_.toString()) + val classpathClassFiles: Seq[String] = androidPackagedClassfiles() + .filter(_.path.ext == "class") + .map(_.path.toString) - val appCompiledFiles = os.walk(compile().classes.path) - .filter(_.ext == "class") - .map(_.toString) + val appCompiledFiles: Seq[String] = androidPackagedCompiledClasses() + .filter(_.path.ext == "class") + .map(_.path.toString) - T.log.debug(s"appCompiledFiles: ${appCompiledFiles}") + val allClassFiles = classpathClassFiles ++ appCompiledFiles val r8ArgsBuilder = Seq.newBuilder[String] @@ -835,10 +1003,18 @@ trait AndroidAppModule extends AndroidModule { configOut.toString ) - if (!enableDesugaring()) { + 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, @@ -867,104 +1043,30 @@ trait AndroidAppModule extends AndroidModule { // ProGuard configuration files: add our extra rules file and all provided config files. val pgArgs = Seq("--pg-conf", extraRulesFile.toString) ++ - proguardConfigs().flatMap(cfg => Seq("--pg-conf", cfg.path.toString)) + androidProguardReleaseConfigs().flatMap(cfg => Seq("--pg-conf", cfg.path.toString)) r8ArgsBuilder ++= pgArgs - r8ArgsBuilder ++= appCompiledFiles + r8ArgsBuilder ++= allClassFiles val r8Args = r8ArgsBuilder.result() - T.log.info(s"Running r8 with the command: ${r8Args.mkString(" ")}") - - val result = os.call(r8Args) - - T.log.info(result.out.text()) - - if (result.exitCode != 0) { - T.log.error(s"R8 failed with exit code ${result.exitCode}") - T.log.error(result.err.text()) - throw new RuntimeException(s"R8 failed with exit code ${result.exitCode}") - } - - PathRef(outputPath) - } - - def androidReleaseInstall: T[PathRef] = Task { - val unsignedApk = Task.dest / "app.unsigned.apk" - os.copy(androidResources()._1.path / "res.apk", unsignedApk) - - val r8DexFiles = os.walk(runR8().path) - .filter(_.ext == "dex") - .map(os.zip.ZipSource.fromPath) - - 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) - - os.zip(unsignedApk, r8DexFiles) - os.zip(unsignedApk, metaInf) - - val alignedApk: os.Path = Task.dest / "app.aligned.apk" - - os.call(( - androidSdkModule().zipalignPath().path.toString, - "-f", - "-p", - "4", - unsignedApk.toString, - alignedApk - )) - - val signedApk = Task.dest / "app.apk" - - val signArgs = Seq( - androidSdkModule().apksignerPath().path.toString, - "sign", - "--in", - alignedApk.toString, - "--out", - signedApk.toString - ) ++ androidSignKeyDetails() - - T.log.info(s"Calling apksigner with arguments: ${signArgs.mkString(" ")}") - - os.call(signArgs) - - val emulator = runningEmulator() - - os.call( - (androidSdkModule().adbPath().path, "-s", emulator, "install", "-r", signedApk.toString) - ) - - PathRef(signedApk) - + PathRef(outputPath) -> r8Args } trait AndroidAppTests extends AndroidAppModule with JavaTests { - override def androidCompileSdk: T[Int] = parent.androidCompileSdk() - override def androidMinSdk: T[Int] = parent.androidMinSdk() - override def androidTargetSdk: T[Int] = parent.androidTargetSdk() - override def androidSdkModule: ModuleRef[AndroidSdkModule] = parent.androidSdkModule - override def androidManifest: T[PathRef] = parent.androidManifest() + override def androidCompileSdk: T[Int] = outer.androidCompileSdk() + override def androidMinSdk: T[Int] = outer.androidMinSdk() + override def androidTargetSdk: T[Int] = outer.androidTargetSdk() + override def androidSdkModule: ModuleRef[AndroidSdkModule] = outer.androidSdkModule + override def androidManifest: T[PathRef] = outer.androidManifest() - override def androidApplicationId: String = parent.androidApplicationId + override def androidApplicationId: String = outer.androidApplicationId - override def androidApplicationNamespace: String = parent.androidApplicationNamespace + override def androidApplicationNamespace: String = outer.androidApplicationNamespace - override def moduleDir = parent.moduleDir + override def moduleDir = outer.moduleDir override def sources: T[Seq[PathRef]] = Task.Sources("src/test/java") @@ -978,26 +1080,26 @@ trait AndroidAppModule extends AndroidModule { } trait AndroidAppInstrumentedTests extends AndroidAppModule with AndroidTestModule { - override def moduleDir = parent.moduleDir + override def moduleDir = outer.moduleDir - override def moduleDeps: Seq[JavaModule] = Seq(parent) + override def moduleDeps: Seq[JavaModule] = Seq(outer) - override def androidCompileSdk: T[Int] = parent.androidCompileSdk() - override def androidMinSdk: T[Int] = parent.androidMinSdk() - override def androidTargetSdk: T[Int] = parent.androidTargetSdk() + override def androidCompileSdk: T[Int] = outer.androidCompileSdk() + override def androidMinSdk: T[Int] = outer.androidMinSdk() + override def androidTargetSdk: T[Int] = outer.androidTargetSdk() - override def androidIsDebug: T[Boolean] = parent.androidIsDebug() + override def androidIsDebug: T[Boolean] = Task { true } - override def androidApplicationId: String = parent.androidApplicationId - override def androidApplicationNamespace: String = parent.androidApplicationNamespace + override def androidApplicationId: String = s"${outer.androidApplicationId}.test" + override def androidApplicationNamespace: String = outer.androidApplicationNamespace - override def androidReleaseKeyAlias: T[Option[String]] = parent.androidReleaseKeyAlias() - override def androidReleaseKeyName: T[Option[String]] = parent.androidReleaseKeyName() - override def androidReleaseKeyPass: T[Option[String]] = parent.androidReleaseKeyPass() - override def androidReleaseKeyStorePass: T[Option[String]] = parent.androidReleaseKeyStorePass() - override def androidReleaseKeyPath: T[Option[PathRef]] = parent.androidReleaseKeyPath() + override def androidReleaseKeyAlias: T[Option[String]] = outer.androidReleaseKeyAlias() + override def androidReleaseKeyName: T[Option[String]] = outer.androidReleaseKeyName() + override def androidReleaseKeyPass: T[Option[String]] = outer.androidReleaseKeyPass() + override def androidReleaseKeyStorePass: T[Option[String]] = outer.androidReleaseKeyStorePass() + override def androidReleaseKeyPath: T[Option[PathRef]] = outer.androidReleaseKeyPath() - override def androidEmulatorPort: String = parent.androidEmulatorPort + override def androidEmulatorPort: String = outer.androidEmulatorPort override def sources: T[Seq[PathRef]] = Task.Sources("src/androidTest/java") @@ -1010,39 +1112,73 @@ trait AndroidAppModule extends AndroidModule { override def generatedSources: T[Seq[PathRef]] = Task.Sources() - /* TODO on debug work, an AndroidManifest.xml with debug and instrumentation settings - * will need to be created. Then this needs to point to the location of that debug - * AndroidManifest.xml + private def androidInstrumentedTestsBaseManifest: Task[Elem] = Task.Anon { + + {androidManifestUsesSdkSection()} + + } + + /** + * The android manifest of the instrumented tests + * has a different package from the app to differentiate installations + * @return */ - override def androidManifest: T[PathRef] = parent.androidManifest() + 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) + PathRef(destManifest) - override def androidVirtualDeviceIdentifier: String = parent.androidVirtualDeviceIdentifier - override def androidEmulatorArchitecture: String = parent.androidEmulatorArchitecture + } - def instrumentationPackage: String + override def androidVirtualDeviceIdentifier: String = outer.androidVirtualDeviceIdentifier + override def androidEmulatorArchitecture: String = outer.androidEmulatorArchitecture def testFramework: T[String] - override def androidInstall(): Command[String] = Task.Command { - val emulator = runningEmulator() + /** + * Re/Installs the app apk and then the test apk on the [[runningEmulator]] + * @return + */ + def androidTestInstall(): Command[String] = Task.Command { + + val emulator = outer.androidInstallTask() + os.call( ( androidSdkModule().adbPath().path, "-s", emulator, "install", - "-r", - androidInstantApk().path + "-t", + androidTestApk().path ) ) emulator } + /** + * Runs the tests on the [[runningEmulator]] with the [[androidTestApk]] + * against the [[androidApk]] + * @param args + * @param globSelectors + * @return + */ override def testTask( args: Task[Seq[String]], globSelectors: Task[Seq[String]] - ): Task[(String, Seq[TestResult])] = Task { - val device = androidInstall().apply() + ): Task[(String, Seq[TestResult])] = Task.Anon { + val device = androidTestInstall().apply() val instrumentOutput = os.proc( ( @@ -1054,7 +1190,7 @@ trait AndroidAppModule extends AndroidModule { "instrument", "-w", "-r", - s"$instrumentationPackage/${testFramework()}" + s"${androidApplicationId}/${testFramework()}" ) ).spawn() @@ -1067,8 +1203,30 @@ trait AndroidAppModule extends AndroidModule { } + /** The instrumented dex should just contain the test dependencies and locally tested files */ + override def androidPackagedClassfiles: T[Seq[PathRef]] = Task { + testClasspath() + .map(_.path).filter(os.isDir) + .flatMap(os.walk(_)) + .filter(os.isFile) + .filter(_.ext == "class") + .map(PathRef(_)) + } + + override def androidPackagedDeps: T[Seq[PathRef]] = Task { + resolvedRunMvnDeps() + } + + /** + * The instrumented tests are packaged with testClasspath which already contains the + * user compiled classes + */ + override def androidPackagedCompiledClasses: T[Seq[PathRef]] = Task { + Seq.empty[PathRef] + } + /** Builds the apk including the integration tests (e.g. from androidTest) */ - def androidInstantApk: T[PathRef] = androidApk() + def androidTestApk: T[PathRef] = androidApk() @internal override def bspBuildTarget: BspBuildTarget = super[AndroidTestModule].bspBuildTarget.copy( diff --git a/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala b/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala new file mode 100644 index 000000000000..e770d74bedf4 --- /dev/null +++ b/libs/androidlib/src/mill/androidlib/AndroidBuildTypeSettings.scala @@ -0,0 +1,38 @@ +package mill.androidlib + +import mill.define.JsonFormatters.pathReadWrite + +/** + * Build type settings for + * various packaging configurations. + * See also [[https://developer.android.com/build/build-variants#build-types]] + * + * Useful for getting different packaging strategies for code shrinking and + * supporting build variants + */ +case class AndroidBuildTypeSettings( + isMinifyEnabled: Boolean = false, + isShrinkEnabled: Boolean = false, + enableDesugaring: Boolean = false, + proguardFiles: ProguardFiles = ProguardFiles() +) { + def withProguardLocalFiles(localFiles: Seq[os.Path]): AndroidBuildTypeSettings = + copy(proguardFiles = proguardFiles.copy(localFiles = localFiles)) + + def withDefaultProguardFile(fileName: String): AndroidBuildTypeSettings = + copy(proguardFiles = proguardFiles.copy(defaultProguardFile = Some(fileName))) +} + +case class ProguardFiles( + defaultProguardFile: Option[String] = None, + localFiles: Seq[os.Path] = List.empty +) + +object AndroidBuildTypeSettings { + implicit val resultRW: upickle.default.ReadWriter[AndroidBuildTypeSettings] = + upickle.default.macroRW +} + +object ProguardFiles { + implicit val resultRW: upickle.default.ReadWriter[ProguardFiles] = upickle.default.macroRW +} diff --git a/libs/androidlib/src/mill/androidlib/AndroidLibModule.scala b/libs/androidlib/src/mill/androidlib/AndroidLibModule.scala index 27a41547f42f..d65088d04d5d 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidLibModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidLibModule.scala @@ -7,10 +7,38 @@ import mill.scalalib.publish.{PackagingType, PublishInfo} import mill.util.Jvm import os.RelPath import upickle.default.* +import scala.xml.* @mill.api.experimental trait AndroidLibModule extends AndroidModule with PublishModule { + /** + * The package name of the module. Used in the generated AndroidManifest.xml + * and for placing the android resources in. + * @return + */ + def androidLibPackage: String + + override protected def androidGeneratedResourcesPackage: String = androidLibPackage + + /** + * Provides os.Path to an XML file containing configuration and metadata about your android application. + * TODO dynamically add android:debuggable + */ + override def androidManifest: T[PathRef] = Task { + val manifestFromSourcePath = moduleDir / "src/main/AndroidManifest.xml" + + val manifestElem = XML.loadFile(manifestFromSourcePath.toString()) + // add the application package + val manifestWithPackage = + manifestElem % Attribute(None, "package", Text(androidLibPackage), Null) + + val generatedManifestPath = Task.dest / "AndroidManifest.xml" + os.write(generatedManifestPath, manifestWithPackage.mkString) + + PathRef(generatedManifestPath) + } + /** * The packaging type of the module. This is used to determine how the module * should be published. For Android libraries, this is always Aar. diff --git a/libs/androidlib/src/mill/androidlib/AndroidModule.scala b/libs/androidlib/src/mill/androidlib/AndroidModule.scala index 5cc92229fd5c..870d54384d0e 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidModule.scala @@ -2,7 +2,6 @@ package mill.androidlib import coursier.Repository import mill.T -import mill.androidlib.AndroidModule.AndroidModuleGeneratedSources import mill.define.{ModuleRef, PathRef, Target, Task} import mill.scalalib.* import mill.util.Jvm @@ -15,10 +14,6 @@ trait AndroidModule extends JavaModule { private val rClassDirName = "RClass" private val compiledResourcesDirName = "compiled-resources" - protected val debugKeyStorePass = "mill-android" - protected val debugKeyAlias = "mill-android" - protected val debugKeyPass = "mill-android" - // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/D8BundleMainDexListTask.kt;l=210-223;drc=66ab6bccb85ce3ed7b371535929a69f494d807f0 val mainDexPlatformRules = Seq( "-keep public class * extends android.app.Instrumentation {\n" + @@ -51,7 +46,7 @@ trait AndroidModule extends JavaModule { def androidSdkModule: ModuleRef[AndroidSdkModule] /** - * Provides os.Path to an XML file containing configuration and metadata about your android application. + * Provides os.Path to an XML file containing configuration and metadata about your android library. */ def androidManifest: Task[PathRef] = Task.Source("src/main/AndroidManifest.xml") @@ -60,7 +55,9 @@ trait AndroidModule extends JavaModule { * * This option will probably go away in the future once build variants are supported. */ - def androidIsDebug: T[Boolean] = true + def androidIsDebug: T[Boolean] = { + true + } /** * The minimum SDK version to use. Default is 1. @@ -118,56 +115,6 @@ trait AndroidModule extends JavaModule { super.repositoriesTask() :+ AndroidSdkModule.mavenGoogle } - private def androidDebugKeystore: Task[PathRef] = Task(persistent = true) { - val debugFileName = "mill-debug.jks" - val globalDebugFileLocation = os.home / ".mill-android" - - if (!os.exists(globalDebugFileLocation)) { - os.makeDir(globalDebugFileLocation) - } - - val debugKeystoreFile = globalDebugFileLocation / debugFileName - - if (!os.exists(debugKeystoreFile)) { - // TODO test on windows and mac and/or change implementation with java APIs - os.call(( - "keytool", - "-genkeypair", - "-keystore", - debugKeystoreFile, - "-alias", - debugKeyAlias, - "-dname", - "CN=MILL, OU=MILL, O=MILL, L=MILL, S=MILL, C=MILL", - "-validity", - "10000", - "-keyalg", - "RSA", - "-keysize", - "2048", - "-storepass", - debugKeyStorePass, - "-keypass", - debugKeyPass - )) - } - - val debugKeystoreTaskFile = T.dest / debugFileName - - os.copy(debugKeystoreFile, debugKeystoreTaskFile) - - PathRef(debugKeystoreTaskFile) - } - - protected def androidKeystore: T[PathRef] = Task { - val pathRef = if (androidIsDebug()) { - androidDebugKeystore() - } else { - androidReleaseKeyPath().get - } - pathRef - } - /** * Classpath for the manifest merger run. */ @@ -298,6 +245,9 @@ trait AndroidModule extends JavaModule { libClasses :+ PathRef(mainRClassPath) } + /** In which package to place the generated R sources */ + protected def androidGeneratedResourcesPackage: String + /** * Compiles Android resources and generates `R.java` and `res.apk`. * @@ -367,6 +317,8 @@ trait AndroidModule extends JavaModule { androidSdkModule().androidJarPath().path.toString, "--manifest", androidMergedManifest().path.toString, + "--custom-package", + androidGeneratedResourcesPackage, "--java", rClassDir.toString, "--min-sdk-version", @@ -430,69 +382,8 @@ trait AndroidModule extends JavaModule { PathRef(rJar) } - // TODO alias, keystore pass and pass below are sensitive credentials and shouldn't be leaked to disk/console. - // In the current state they are leaked, because Task dumps output to the json. - // Should be fixed ASAP. - - /** - * Name of the key alias in the release keystore. Default is not set. - */ - def androidReleaseKeyAlias: T[Option[String]] = Task { - None - } - - /** - * Name of the release keystore file. Default is not set. - */ - def androidReleaseKeyName: T[Option[String]] = Task { - None - } - - /** - * Password for the release key. Default is not set. - */ - def androidReleaseKeyPass: T[Option[String]] = Task { - None - } - - /** - * Password for the release keystore. Default is not set. - */ - def androidReleaseKeyStorePass: T[Option[String]] = Task { - None - } - - /** The emulator port where adb connects to. Defaults to 5554 */ - def androidEmulatorPort: String = "5554" - - /** - * Returns the emulator identifier for created from startAndroidEmulator - * by iterating the adb device list - */ - def runningEmulator: T[String] = Task { - s"emulator-$androidEmulatorPort" - } - - /** - * Default os.Path to the keystore file, derived from `androidReleaseKeyName()`. - * Users can customize the keystore file name to change this path. - */ - def androidReleaseKeyPath: T[Option[PathRef]] = Task { - androidReleaseKeyName().map(name => PathRef(moduleDir / name)) - } - - /** The name of the virtual device to be created by [[createAndroidVirtualDevice]] */ - def androidVirtualDeviceIdentifier: String = "test" - - /** - * The target architecture of the virtual device to be created by [[createAndroidVirtualDevice]] - * For example, "x86_64" (default). For a list of system images and their architectures, - * see the Android SDK Manager `sdkmanager --list`. - */ - def androidEmulatorArchitecture: String = "x86_64" - /** All individual classfiles inherited from the classpath that will be included into the dex */ - def inheritedClassFiles: T[Seq[PathRef]] = Task { + def androidPackagedClassfiles: T[Seq[PathRef]] = Task { compileClasspath() .map(_.path).filter(os.isDir) .flatMap(os.walk(_)) @@ -501,248 +392,16 @@ trait AndroidModule extends JavaModule { .map(PathRef(_)) } - /** - * Converts the generated JAR file into a DEX file using the `d8` tool. - * - * @return os.Path to the Generated DEX File Directory - */ - def androidDex: T[PathRef] = Task { - - val appCompiledFiles = os.walk(compile().classes.path) + def androidPackagedCompiledClasses: T[Seq[PathRef]] = Task { + os.walk(compile().classes.path) .filter(_.ext == "class") - .map(_.toString) ++ inheritedClassFiles().map(_.path.toString) - - val libsJarFiles = compileClasspath() - .filter(_ != androidSdkModule().androidJarPath()) - .filter(_.path.ext == "jar") - .map(_.path.toString()) - - val proguardFile = T.dest / "proguard-rules.pro" - val knownProguardRules = 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(androidResources()._1.path / "main-dex-rules.pro")) - .mkString("\n") - os.write(proguardFile, knownProguardRules) - - val d8ArgsBuilder = Seq.newBuilder[String] - - d8ArgsBuilder += androidSdkModule().d8Path().path.toString - - if (androidIsDebug()) { - d8ArgsBuilder += "--debug" - } else { - d8ArgsBuilder += "--release" - } - // TODO explore --incremental flag for incremental builds - d8ArgsBuilder ++= Seq( - "--output", - Task.dest.toString(), - "--lib", - androidSdkModule().androidJarPath().path.toString(), - "--min-api", - androidMinSdk().toString, - "--main-dex-rules", - proguardFile.toString() - ) ++ appCompiledFiles ++ libsJarFiles - - val d8Args = d8ArgsBuilder.result() - - T.log.info(s"Running d8 with the command: ${d8Args.mkString(" ")}") - - os.call(d8Args) - - PathRef(Task.dest) - } - - private def isExcludedFromPackaging(relPath: RelPath): Boolean = { - val topPath = relPath.segments.head - // TODO do this better - // full list is here https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/packaging/PackagingOptionsUtils.kt;drc=85330e2f750acc1e1510623222d80e4b1ad5c8a2 - // but anyway it should be a packaging option DSL to configure additional excludes from the user side - relPath.ext == "kotlin_module" || - relPath.ext == "kotlin_metadata" || - relPath.ext == "DSA" || - relPath.ext == "EC" || - relPath.ext == "SF" || - relPath.ext == "RSA" || - topPath == "maven" || - topPath == "proguard" || - topPath == "com.android.tools" || - relPath.last == "MANIFEST.MF" || - relPath.last == "LICENSE" || - relPath.last == "LICENSE.TXT" || - relPath.last == "NOTICE" || - relPath.last == "NOTICE.TXT" || - relPath.last == "kotlin-project-structure-metadata.json" || - relPath.last == "module-info.class" - } - - /** - * Collect files from META-INF folder of classes.jar (not META-INF of aar in case of Android library). - */ - def androidLibsClassesJarMetaInf: T[Seq[PathRef]] = Task { - // ^ not the best name for the method, but this is to distinguish between META-INF of aar and META-INF - // of classes.jar included in aar - compileClasspath() - .filter(ref => - ref.path.ext == "jar" && - ref != androidSdkModule().androidJarPath() - ) - .flatMap(ref => { - val dest = T.dest / ref.path.baseName - os.unzip(ref.path, dest) - val lookupPath = dest / "META-INF" - if (os.exists(lookupPath)) { - os.walk(lookupPath) - .filter(os.isFile) - .filterNot(f => isExcludedFromPackaging(f.relativeTo(lookupPath))) - } else { - Seq.empty[os.Path] - } - }) .map(PathRef(_)) - .toSeq - } - - /** - * Packages DEX files and Android resources into an unsigned APK. - * - * @return A `PathRef` to the generated unsigned APK file (`app.unsigned.apk`). - */ - def androidUnsignedApk: T[PathRef] = Task { - val unsignedApk = Task.dest / "app.unsigned.apk" - - os.copy(androidResources()._1.path / "res.apk", unsignedApk) - 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) - - // TODO generate aar-metadata.properties (for lib distribution, not in this module) or - // app-metadata.properties (for app distribution). - // Example of aar-metadata.properties: - // aarFormatVersion=1.0 - // aarMetadataVersion=1.0 - // minCompileSdk=33 - // minCompileSdkExtension=0 - // minAndroidGradlePluginVersion=1.0.0 - // - // Example of app-metadata.properties: - // appMetadataVersion=1.1 - // androidGradlePluginVersion=8.7.2 - os.zip(unsignedApk, dexFiles) - os.zip(unsignedApk, metaInf) - // TODO pack also native (.so) libraries - - PathRef(unsignedApk) - } - - /** - * Generates the command-line arguments required for Android app signing. - * - * Uses the release keystore if release build type is set; otherwise, defaults to a generated debug keystore. - */ - def androidSignKeyDetails: T[Seq[String]] = Task { - - val keystorePass = { - if (androidIsDebug()) debugKeyStorePass else androidReleaseKeyStorePass().get - } - val keyAlias = { - if (androidIsDebug()) debugKeyAlias else androidReleaseKeyAlias().get - } - val keyPass = { - if (androidIsDebug()) debugKeyPass else androidReleaseKeyPass().get - } - - Seq( - "--ks", - androidKeystore().path.toString, - "--ks-key-alias", - keyAlias, - "--ks-pass", - s"pass:$keystorePass", - "--key-pass", - s"pass:$keyPass" - ) - } - - /** - * Optimizes the APK using the `zipalign` tool for better performance. - * - * For more details on the zipalign tool, refer to: - * [[https://developer.android.com/tools/zipalign zipalign Documentation]] - */ - def androidAlignedUnsignedApk: T[PathRef] = Task { - val alignedApk: os.Path = Task.dest / "app.aligned.apk" - - os.call(( - androidSdkModule().zipalignPath().path, - "-f", - "-p", - "4", - androidUnsignedApk().path, - alignedApk - )) - - PathRef(alignedApk) - } - - /** - * Signs the APK using a keystore to generate a final, distributable APK. - * - * The signing step is mandatory to distribute Android applications. It adds a cryptographic - * signature to the APK, verifying its authenticity. This method uses the `apksigner` tool - * along with a keystore file to sign the APK. - * - * If no keystore is available, a new one is generated using the `keytool` utility. - * - * For more details on the apksigner tool, refer to: - * [[https://developer.android.com/tools/apksigner apksigner Documentation]] - */ - def androidApk: T[PathRef] = Task { - val signedApk = Task.dest / "app.apk" - - val signArgs = Seq( - androidSdkModule().apksignerPath().path.toString, - "sign", - "--in", - androidAlignedUnsignedApk().path.toString, - "--out", - signedApk.toString - ) ++ androidSignKeyDetails() - - T.log.info(s"Calling apksigner with arguments: ${signArgs.mkString(" ")}") - - os.call(signArgs) - - PathRef(signedApk) } - /** ProGuard/R8 rules configuration files (user-provided and generated) */ - def proguardConfigs: T[Seq[PathRef]] = { - Seq( - PathRef(moduleDir / "proguard-rules.pro"), - PathRef(androidModuleGeneratedSourcesFunc().androidReleaseDex.path / "proguard-rules.pro"), - PathRef(moduleDir / "test-proguard-rules.pro") - ) + def androidPackagedDeps: T[Seq[PathRef]] = Task { + compileClasspath() + .filter(_ != androidSdkModule().androidJarPath()) + .filter(_.path.ext == "jar") } /** Additional library classes provided */ @@ -750,10 +409,6 @@ trait AndroidModule extends JavaModule { androidSdkModule().androidLibsClasspaths() } - def enableDesugaring: T[Boolean] = Task { - true - } - /** * Specifies the path to the main-dex rules file, which contains ProGuard rules * for determining which classes should be included in the primary DEX file. @@ -770,68 +425,9 @@ trait AndroidModule extends JavaModule { None } - /** - * Provides the output path for the generated main-dex list file, which is used - * during the DEX generation process. - */ - def mainDexListOutput: T[Option[PathRef]] = Task { - Some(androidModuleGeneratedSourcesFunc().mainDexListOutput) - } - /** Optional baseline profile for ART rewriting */ def baselineProfile: T[Option[PathRef]] = Task { None } - def androidModuleGeneratedSourcesFunc: T[AndroidModuleGeneratedSources] = Task { - val androidDebugDex = T.dest / "androidDebugDex.dest" - os.makeDir(androidDebugDex) - val androidReleaseDex = T.dest / "androidReleaseDex.dest" - os.makeDir(androidReleaseDex) - val mainDexListOutput = T.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(androidResources()._1.path / "main-dex-rules.pro")) - .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(androidResources()._1.path / "main-dex-rules.pro")) - .mkString("\n") - os.write(proguardFileRelease, knownProguardRulesRelease) - - AndroidModuleGeneratedSources( - androidDebugDex = PathRef(androidDebugDex), - androidReleaseDex = PathRef(androidReleaseDex), - mainDexListOutput = PathRef(mainDexListOutput) - ) - } - -} - -object AndroidModule { - case class AndroidModuleGeneratedSources( - androidDebugDex: PathRef, - androidReleaseDex: PathRef, - mainDexListOutput: PathRef - ) - - object AndroidModuleGeneratedSources { - implicit def resultRW: upickle.default.ReadWriter[AndroidModuleGeneratedSources] = - upickle.default.macroRW - } } diff --git a/libs/androidlib/src/mill/androidlib/AndroidModuleGeneratedDexVariants.scala b/libs/androidlib/src/mill/androidlib/AndroidModuleGeneratedDexVariants.scala new file mode 100644 index 000000000000..d1274f298b81 --- /dev/null +++ b/libs/androidlib/src/mill/androidlib/AndroidModuleGeneratedDexVariants.scala @@ -0,0 +1,14 @@ +package mill.androidlib + +import mill.define.PathRef + +case class AndroidModuleGeneratedDexVariants( + androidDebugDex: PathRef, + androidReleaseDex: PathRef, + mainDexListOutput: PathRef +) + +object AndroidModuleGeneratedDexVariants { + implicit def resultRW: upickle.default.ReadWriter[AndroidModuleGeneratedDexVariants] = + upickle.default.macroRW +} diff --git a/libs/androidlib/src/mill/androidlib/AndroidSdkModule.scala b/libs/androidlib/src/mill/androidlib/AndroidSdkModule.scala index 1c6987bed631..a546bb580366 100644 --- a/libs/androidlib/src/mill/androidlib/AndroidSdkModule.scala +++ b/libs/androidlib/src/mill/androidlib/AndroidSdkModule.scala @@ -198,6 +198,14 @@ trait AndroidSdkModule extends Module { PathRef(sdkPath().path / "emulator/emulator") } + /** + * Location of the default proguard optimisation config. + * See also [[https://developer.android.com/build/shrink-code]] + */ + def androidProguardPath: T[PathRef] = Task { + PathRef(sdkPath().path / "tools/proguard") + } + /** * Provides the path for the Android SDK Manager tool *