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
*