From d6bcf8529ead05c3517f30d65e004772f8758168 Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Thu, 3 Oct 2024 19:54:33 +0530 Subject: [PATCH 1/4] Fixes: #3550 ; Add hello-world Android Java Example using Mill --- .../javalib/android/1-hello-world/build.mill | 63 +++++ .../src/main/AndroidManifest.xml | 13 + .../java/com/helloworld/app/MainActivity.java | 36 +++ example/package.mill | 1 + .../javalib/android/AndroidAppModule.scala | 262 ++++++++++++++++++ .../javalib/android/AndroidSdkModule.scala | 173 ++++++++++++ 6 files changed, 548 insertions(+) create mode 100644 example/javalib/android/1-hello-world/build.mill create mode 100644 example/javalib/android/1-hello-world/src/main/AndroidManifest.xml create mode 100644 example/javalib/android/1-hello-world/src/main/java/com/helloworld/app/MainActivity.java create mode 100644 scalalib/src/mill/javalib/android/AndroidAppModule.scala create mode 100644 scalalib/src/mill/javalib/android/AndroidSdkModule.scala diff --git a/example/javalib/android/1-hello-world/build.mill b/example/javalib/android/1-hello-world/build.mill new file mode 100644 index 00000000000..bee34f34d2e --- /dev/null +++ b/example/javalib/android/1-hello-world/build.mill @@ -0,0 +1,63 @@ +// This Example demonstrates a simple "Hello World" +// Android application built using the [Mill build tool](https://www.lihaoyi.com/mill/). + +//// SNIPPET:BUILD +package build + +import mill._ +import mill.javalib.android.AndroidAppModule + +// Defines an Android app build module using Mill, extending AndroidAppModule. +object App extends AndroidAppModule { + + // Default project root to be one level up from the current millSourcePath + def projectRoot: T[os.Path] = T { + os.Path(millSourcePath.toString.replace("App", "")) + } + // The name of the Android application, default is "HelloWorld". + def appName: T[String] = T { "HelloWorld" } +} + +////SNIPPET:END + +/** Usage + +> ./mill App.createApp + +*/ + +// This is a basic Mill build for creating Simple `Hello-World` Android Application +// which extends `AndroidAppModule` for all Andorid Application related tasks, +// here we can alter the default values of `projectRoot` +// (Which Contains All Files required for Android Application Creation) and +// `appName` (responsible For Final Application Name). +// User can Change these values according to their need and the Project Folder Structure +// for this would look something like this: +// +// ---- +// . +// ├── build.mill +// └── src +// └── main +// ├── AndroidManifest.xml +// └── java +// └── com +// └── helloworld +// └── app +// └── MainActivity.java +// +// ---- +// +//// SNIPPET:Modules +// +// This example project uses `AndroidAppModule` and this Module further depends on two Modules +// `AndroidSdkModule`(For Installing Andorid SDK and Tools Required to create Android Application) +// and `JavaModule` (For Using Java Commands for Compiling, Creating Jars and common java tasks) +// all the code works in a flow from installing Android SDK to creating Android Application. + +// Users can Perform Manual Testing to insure Proper Functioning of the Application and Automatic +// Testing Procedure is still under Observance once Approved, will be shared. + +// Mill Build Tool already have Advantages over the other Build Tools and +// having the support for Android Application with Mill Build Tool Provdies Efficieny and +// Optimisation to Android Application Creation Process. \ No newline at end of file diff --git a/example/javalib/android/1-hello-world/src/main/AndroidManifest.xml b/example/javalib/android/1-hello-world/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..b33d6eb4174 --- /dev/null +++ b/example/javalib/android/1-hello-world/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/example/javalib/android/1-hello-world/src/main/java/com/helloworld/app/MainActivity.java b/example/javalib/android/1-hello-world/src/main/java/com/helloworld/app/MainActivity.java new file mode 100644 index 00000000000..1883d567555 --- /dev/null +++ b/example/javalib/android/1-hello-world/src/main/java/com/helloworld/app/MainActivity.java @@ -0,0 +1,36 @@ +package com.helloworld.app; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; +import android.view.ViewGroup.LayoutParams; +import android.view.Gravity; + + +public class MainActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Create a new TextView + TextView textView = new TextView(this); + + // Set the text to "Hello, World!" + textView.setText("Hello, World!"); + + // Set text size + textView.setTextSize(32); + + // Center the text within the view + textView.setGravity(Gravity.CENTER); + + // Set layout parameters (width and height) + textView.setLayoutParams(new LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + + // Set the content view to display the TextView + setContentView(textView); + } +} diff --git a/example/package.mill b/example/package.mill index 9544d316c76..2c8e305ce92 100644 --- a/example/package.mill +++ b/example/package.mill @@ -28,6 +28,7 @@ object `package` extends RootModule with Module { .collect { case m: ExampleCrossModule => m } object javalib extends Module { + object android extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "android")) object basic extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "basic")) object builds extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "builds")) object testing extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "testing")) diff --git a/scalalib/src/mill/javalib/android/AndroidAppModule.scala b/scalalib/src/mill/javalib/android/AndroidAppModule.scala new file mode 100644 index 00000000000..4e5dfe3cf16 --- /dev/null +++ b/scalalib/src/mill/javalib/android/AndroidAppModule.scala @@ -0,0 +1,262 @@ +package mill.javalib.android + +import mill._ +import mill.api.PathRef +import mill.scalalib.JavaModule +import mill.javalib.android.AndroidSdkModule +import mill.util.Jvm + +/** + * Trait for building Android applications using Mill, + * this extends [[AndroidSdkModule]] for Android SDK related tasks, + * and [[JavaModule]] for Java related tasks. + * + * This trait outlines the steps necessary to build an Android application: + * 1. Compile Java code into `.class` files. + * 2. Package the `.class` files into a JAR file. + * 3. Convert the JAR into DEX format for Android. + * 4. Package DEX files and resources into an APK. + * 5. Optimize the APK using zipalign. + * 6. Sign the APK for distribution. + * + * For detailed information, refer to Mill's [documentation](https://com-lihaoyi.github.io/mill), + * and the [Android developer guide](https://developer.android.com/studio). + */ +trait AndroidAppModule extends AndroidSdkModule with JavaModule { + + /** + * Path where the Project related Files will live. + * + * @return A `PathRef` representing project directory. + */ + def projectRoot: T[os.Path] = T { + os.Path(millSourcePath.toString.replace( + "App", + "" + )) // Get the parent directory of millSourcePath + } + + /** + * App Name for the Application default is HelloWorld. + * + * @return A string representing the platform version. + */ + def appName: T[String] = T { "HelloWorld" } + + /** + * Step 1: Compile Java source files to `.class` files. + * + * This method: + * - Ensures the Android SDK is installed. + * - Compiles all `.java` files in `src/main/java` to `.class` files stored in `obj/` directory. + * + * @return A `PathRef` to the directory containing the compiled `.class` files. + * + * @see [[createJar]] + */ + def compileJava: T[PathRef] = T { + installAndroidSdk() // Step 1: Install the Android SDK if not already done. + val outputDir = T.dest / "obj" // Directory to store compiled class files. + + os.call( + Seq( + Jvm.jdkTool("javac"), // Use the Java compiler + "-classpath", + androidJarPath().path.toString, // Include Android framework classes + "-d", + outputDir.toString // Specify output directory for class files + ) ++ os.walk(projectRoot() / "src/main/java").filter(_.ext == "java").map( + _.toString + ) // Get all Java source files + ) + + PathRef(outputDir) // Return the path to compiled class files. + } + + /** + * Step 2: Package `.class` files into a JAR file. + * + * This method: + * - Converts the compiled `.class` files into a JAR file using the `d8` tool. + * + * @return A `PathRef` to the generated JAR file. + * + * @see [[compileJava]] + * @see [[createDex]] + */ + def createJar: T[PathRef] = T { + val jarFile = T.dest / "my_classes.jar" // Specify output JAR file name. + + os.call( + Seq( + d8Path().path.toString, // Path to the D8 tool + "--output", + jarFile.toString, // Output JAR file + "--no-desugaring" // Do not apply desugaring + ) ++ os.walk(compileJava().path).filter(_.ext == "class").map( + _.toString + ) // Get compiled class files from compileJava + ) + + PathRef(jarFile) // Return the path to the created JAR file. + } + + /** + * Step 3: Convert the JAR file into a DEX file. + * + * This method: + * - Uses the `d8` tool to convert the JAR file into DEX format, required for Android apps. + * + * @return A `PathRef` to the generated DEX file. + * + * @see [[createJar]] + */ + def createDex: T[PathRef] = T { + val dexOutputDir = T.dest // Directory to store DEX files. + + os.call( + Seq(d8Path().path.toString, "--output", dexOutputDir.toString) ++ Seq( + createJar().path.toString, // Use the JAR file from createJar + androidJarPath().path.toString // Include Android framework classes + ) + ) + + PathRef(dexOutputDir) // Return the path to the generated DEX file. + } + + /** + * Step 4: Package the DEX file into an unsigned APK. + * + * This method: + * - Uses the `aapt` tool to create an APK file that includes the DEX file and resources. + * + * @return A `PathRef` to the unsigned APK file. + * + * @see [[createDex]] + */ + def createApk: T[PathRef] = T { + val unsignedApk = + T.dest / s"${appName().toString}.unsigned.apk" // Specify output APK file name. + + os.call( + Seq( + aaptPath().path.toString, + "package", // Command to package APK + "-f", // Force overwrite + "-M", + (projectRoot() / "src/main/AndroidManifest.xml").toString, // Path to the AndroidManifest.xml + "-I", + androidJarPath().path.toString, // Include Android framework resources + "-F", + unsignedApk.toString // Specify output APK file + ) ++ Seq(createDex().path.toString) // Include the DEX file from createDex + ) + + PathRef(unsignedApk) // Return the path to the unsigned APK. + } + + /** + * Step 5: Optimize the APK using zipalign. + * + * This method: + * - Takes the unsigned APK and optimizes it for better performance on Android devices. + * + * @return A `PathRef` to the aligned APK file. + * + * @see [[createApk]] + */ + def alignApk: T[PathRef] = T { + val alignedApk = + T.dest / s"${appName().toString}.aligned.apk" // Specify output aligned APK file name. + + os.call( + Seq( + zipalignPath().path.toString, // Path to the zipalign tool + "-f", + "-p", + "4", // Force overwrite and align with a page size of 4 + createApk().path.toString, // Use the unsigned APK from createApk + alignedApk.toString // Specify output aligned APK file + ) + ) + + PathRef(alignedApk) // Return the path to the aligned APK. + } + + /** + * Step 6: Sign the APK using a keystore. + * + * This method: + * - Signs the aligned APK with a keystore. If the keystore does not exist, it generates one. + * + * @return A `PathRef` to the signed APK file. + * + * @see [[alignApk]] + * @see [[createKeystore]] + */ + def createApp: T[PathRef] = T { + val signedApk = + projectRoot() / s"${appName().toString}.apk" // Specify output signed APK file name. + + os.call( + Seq( + apksignerPath().path.toString, + "sign", // Command to sign APK + "--ks", + createKeystore().path.toString, // Use the keystore from createKeystore + "--ks-key-alias", + "androidkey", // Alias for the key + "--ks-pass", + "pass:android", // Keystore password + "--key-pass", + "pass:android", // Key password + "--out", + signedApk.toString, // Specify output signed APK file + alignApk().path.toString // Use the aligned APK from alignApk + ) + ) + + PathRef(signedApk) // Return the path to the signed APK. + } + + /** + * Creates a keystore for signing APKs if it doesn't already exist. + * + * This method: + * - Generates a keystore file using the `keytool` command. + * + * @return A `PathRef` to the keystore file. + * + * @see [[createApp]] + */ + def createKeystore: T[PathRef] = T { + val keystoreFile = T.dest / "keystore.jks" // Specify keystore file name. + + if (!os.exists(keystoreFile)) { + os.call( + Seq( + "keytool", + "-genkeypair", + "-keystore", + keystoreFile.toString, // Generate keystore + "-alias", + "androidkey", // Key alias + "-dname", + "CN=MILL, OU=MILL, O=MILL, L=MILL, S=MILL, C=IN", // Distinguished name + "-validity", + "10000", // Validity period in days + "-keyalg", + "RSA", // Key algorithm + "-keysize", + "2048", // Key size + "-storepass", + "android", // Keystore password + "-keypass", + "android" // Key password + ) + ) + } + + PathRef(keystoreFile) // Return the path to the keystore file. + } +} diff --git a/scalalib/src/mill/javalib/android/AndroidSdkModule.scala b/scalalib/src/mill/javalib/android/AndroidSdkModule.scala new file mode 100644 index 00000000000..f317d8d9f35 --- /dev/null +++ b/scalalib/src/mill/javalib/android/AndroidSdkModule.scala @@ -0,0 +1,173 @@ +package mill.javalib.android + +import mill._ +import mill.define._ + +/** + * Trait for managing the Android SDK in a Mill build. + * + * This trait provides methods for downloading and setting up the Android SDK, + * build tools, and other resources required for Android development. + * + * It simplifies the process of configuring the Android development environment, + * making it easier to build and package Android applications. + * + * For more, refer to the [Android SDK documentation](https://developer.android.com/studio). + */ +trait AndroidSdkModule extends Module { + + /** + * URL to download the Android SDK command-line tools. + * + * @return A string representing the URL for the SDK tools. + */ + def SdkUrl: T[String] = T { + "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" + } + + /** + * Version of Android build tools. + * + * @return A string representing the version of the build tools. + */ + def BuildToolsVersion: T[String] = T { "35.0.0" } + + /** + * Version of Android platform (e.g., Android API level). + * + * @return A string representing the platform version. + */ + def PlatformVersion: T[String] = T { "android-35" } + + /** + * Directory name for the Android command-line tools. + * + * @return A string representing the directory name for the tools. + */ + def ToolsDirName: T[String] = T { "cmdline-tools" } + + /** + * Name of the zip file containing the SDK tools. + * + * @return A string representing the zip file name. + */ + def ZipFileName: T[String] = T { "commandlinetools.zip" } + + /** + * Path where the Android SDK will be installed. + * + * @return A `PathRef` representing the SDK installation directory. + */ + def sdkDirectory: T[PathRef] = T { PathRef(millSourcePath / "android-sdk") } + + /** + * Path to the Android SDK command-line tools directory. + * + * @return A `PathRef` representing the command-line tools directory. + */ + def toolsDirectory: T[PathRef] = T { PathRef(sdkDirectory().path / ToolsDirName().toString) } + + /** + * Path to the Android build tools based on the selected version. + * + * @return A `PathRef` representing the build tools directory. + * + * @see [[BuildToolsVersion]] + */ + def buildToolsPath: T[PathRef] = + T { PathRef(sdkDirectory().path / "build-tools" / BuildToolsVersion().toString) } + + /** + * Path to `android.jar`, required for compiling Android apps. + * + * @return A `PathRef` representing the path to `android.jar`. + * + * @see [[PlatformVersion]] + */ + def androidJarPath: T[PathRef] = + T { PathRef(sdkDirectory().path / "platforms" / PlatformVersion().toString / "android.jar") } + + /** + * Path to the D8 Dex compiler, used to convert Java bytecode to Dalvik bytecode. + * + * @return A `PathRef` representing the path to the D8 compiler. + * + * @see [[buildToolsPath]] + */ + def d8Path: T[PathRef] = T { PathRef(buildToolsPath().path / "d8") } + + /** + * Path to the Android Asset Packaging Tool (AAPT) for handling resources and packaging APKs. + * + * @return A `PathRef` representing the path to the AAPT tool. + * + * @see [[buildToolsPath]] + */ + def aaptPath: T[PathRef] = T { PathRef(buildToolsPath().path / "aapt") } + + /** + * Path to Zipalign, used to optimize APKs. + * + * @return A `PathRef` representing the path to the zipalign tool. + * + * @see [[buildToolsPath]] + */ + def zipalignPath: T[PathRef] = T { PathRef(buildToolsPath().path / "zipalign") } + + /** + * Path to the APK signer tool, used to sign APKs. + * + * @return A `PathRef` representing the path to the APK signer tool. + * + * @see [[buildToolsPath]] + */ + def apksignerPath: T[PathRef] = T { PathRef(buildToolsPath().path / "apksigner") } + + /** + * Installs the Android SDK by downloading the tools, extracting them, + * accepting licenses, and installing necessary components like platform and build tools. + * + * This method: + * - Downloads the SDK command-line tools from the specified URL. + * - Extracts the downloaded zip file into the specified SDK directory. + * - Accepts the SDK licenses required for use. + * - Installs essential components such as platform-tools, build-tools and platforms. + * + * @throws Exception if any step fails during installation. + * + * @see [[SdkUrl]] + * @see [[toolsDirectory]] + * @see [[sdkDirectory]] + * @see [[BuildToolsVersion]] + * @see [[PlatformVersion]] + */ + def installAndroidSdk: T[Unit] = T { + val zipFilePath: os.Path = sdkDirectory().path / ZipFileName().toString + val sdkManagerPath: os.Path = toolsDirectory().path / "bin" / "sdkmanager" + + // Create SDK directory if it doesn't exist + os.makeDir.all(sdkDirectory().path) + + // Download SDK command-line tools + os.write(zipFilePath, requests.get(SdkUrl().toString).bytes) + + // Extract the zip into the SDK directory + os.call(Seq("unzip", zipFilePath.toString, "-d", sdkDirectory().path.toString)) + + // Accept SDK licenses + os.call(Seq( + "bash", + "-c", + s"yes | $sdkManagerPath --licenses --sdk_root=${sdkDirectory().path}" + )) + + // Install platform-tools, build-tools, and platform + os.call(Seq( + sdkManagerPath.toString, + s"--sdk_root=${sdkDirectory().path}", + "platform-tools", + s"build-tools;${BuildToolsVersion().toString}", + s"platforms;${PlatformVersion().toString}" + )) + } +} From 094e256836cfd174015e8e13d284de3de5835fb9 Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Fri, 4 Oct 2024 01:09:24 +0530 Subject: [PATCH 2/4] Fixes: #3550 ; Documentation Optimisation --- docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/javalib/android-examples.adoc | 48 +++++++++++++++++++ .../javalib/android/1-hello-world/build.mill | 29 +++++------ .../javalib/android/AndroidAppModule.scala | 18 ++++++- .../javalib/android/AndroidSdkModule.scala | 8 ++-- 5 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 docs/modules/ROOT/pages/javalib/android-examples.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index ec78d4d5bc2..82532adb85d 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -6,6 +6,7 @@ .xref:javalib/intro.adoc[] * xref:javalib/installation-ide.adoc[] * xref:javalib/builtin-commands.adoc[] +* xref:javalib/android-examples.adoc[] * xref:javalib/module-config.adoc[] * xref:javalib/build-examples.adoc[] * xref:javalib/testing.adoc[] diff --git a/docs/modules/ROOT/pages/javalib/android-examples.adoc b/docs/modules/ROOT/pages/javalib/android-examples.adoc new file mode 100644 index 00000000000..8ee4af33de8 --- /dev/null +++ b/docs/modules/ROOT/pages/javalib/android-examples.adoc @@ -0,0 +1,48 @@ += Android Build Examples +:page-aliases: android_app_examples.adoc + +++++ + +++++ + +This page provides examples of using Mill as a build tool for Android applications. +Each example highlights a specific aspect of Mill, allowing you to configure, test, and build Android apps efficiently. +By the end, you will understand how Mill can manage Android project structures, compile modules, and handle packaging. + +For detailed information, refer to Mill's https://com-lihaoyi.github.io/mill[documentation], +and the https://developer.android.com/studio[Android developer guide]. + +The following APIs are relevant to this documentation: + +* {mill-doc-url}/api/latest/mill/scalalib/AndroidSdkModule.html[`mill.scalalib.AndroidSdkModule`] +* {mill-doc-url}/api/latest/mill/javalib/android/AndroidAppModule.html[`mill.scalalib.AndroidAppModule`] +* {mill-doc-url}/api/latest/mill/scalalib/JavaModule.html[`mill.scalalib.JavaModule`] + +== Simple Android Hello World Application + +include::partial$example/javalib/android/1-hello-world.adoc[] + +Curious about what's happening behind the scenes, +here is some information for `AndroidSdkModule` and `AndroidAppModule` + +`AndroidSdkModule`: + +* This installs Android SDK Tool. + +`AndroidAppModule`: + +* 1. Compile Java code into `.class` files. +* 2. Package the `.class` files into a JAR file. +* 3. Convert the JAR into DEX format for Android. +* 4. Package DEX files and resources into an APK. +* 5. Optimize the APK using zipalign +* 6. Sign the APK for distribution. + +Users can Perform Manual Testing to insure Proper Functioning of the Application and Automatic +Testing Procedure is still under Observance once Approved, will be shared. + +Mill Build Tool already have Advantages over the other Build Tools and +having the support for Android Application with Mill Build Tool Provdies Efficieny and +Optimisation to Android Application Creation Process. \ No newline at end of file diff --git a/example/javalib/android/1-hello-world/build.mill b/example/javalib/android/1-hello-world/build.mill index bee34f34d2e..7de7ad37374 100644 --- a/example/javalib/android/1-hello-world/build.mill +++ b/example/javalib/android/1-hello-world/build.mill @@ -1,5 +1,11 @@ +// This section demonstrates how to set up a basic Android app structure +// with `AndroidSdkModule.scala` and `AndroidAppModule.scala`, utilizing traits for scalability. + +// Basically, `AndroidAppModule` extends `AndroidSdkModule` so we need to call only AndroidAppModule this will +// insure that all tasks are automatically done for AndroidSdkModule, making user expeience easy + // This Example demonstrates a simple "Hello World" -// Android application built using the [Mill build tool](https://www.lihaoyi.com/mill/). +// Android application built using the https://www.lihaoyi.com/mill[Mill build tool]. //// SNIPPET:BUILD package build @@ -7,7 +13,6 @@ package build import mill._ import mill.javalib.android.AndroidAppModule -// Defines an Android app build module using Mill, extending AndroidAppModule. object App extends AndroidAppModule { // Default project root to be one level up from the current millSourcePath @@ -28,9 +33,12 @@ object App extends AndroidAppModule { // This is a basic Mill build for creating Simple `Hello-World` Android Application // which extends `AndroidAppModule` for all Andorid Application related tasks, -// here we can alter the default values of `projectRoot` -// (Which Contains All Files required for Android Application Creation) and +// here we can alter the default values: +// +// `projectRoot`(Which Contains All Files required for Android Application Creation). +// // `appName` (responsible For Final Application Name). +// // User can Change these values according to their need and the Project Folder Structure // for this would look something like this: // @@ -50,14 +58,7 @@ object App extends AndroidAppModule { // //// SNIPPET:Modules // -// This example project uses `AndroidAppModule` and this Module further depends on two Modules -// `AndroidSdkModule`(For Installing Andorid SDK and Tools Required to create Android Application) -// and `JavaModule` (For Using Java Commands for Compiling, Creating Jars and common java tasks) +// This example project uses `AndroidAppModule` and this Module further depends on two Modules: +// * `AndroidSdkModule`(For Installing Andorid SDK and Tools Required to create Android Application) +// * `JavaModule` (For Using Java Commands for Compiling, Creating Jars and common java tasks) // all the code works in a flow from installing Android SDK to creating Android Application. - -// Users can Perform Manual Testing to insure Proper Functioning of the Application and Automatic -// Testing Procedure is still under Observance once Approved, will be shared. - -// Mill Build Tool already have Advantages over the other Build Tools and -// having the support for Android Application with Mill Build Tool Provdies Efficieny and -// Optimisation to Android Application Creation Process. \ No newline at end of file diff --git a/scalalib/src/mill/javalib/android/AndroidAppModule.scala b/scalalib/src/mill/javalib/android/AndroidAppModule.scala index 4e5dfe3cf16..51e1943170f 100644 --- a/scalalib/src/mill/javalib/android/AndroidAppModule.scala +++ b/scalalib/src/mill/javalib/android/AndroidAppModule.scala @@ -12,15 +12,21 @@ import mill.util.Jvm * and [[JavaModule]] for Java related tasks. * * This trait outlines the steps necessary to build an Android application: + * * 1. Compile Java code into `.class` files. + * * 2. Package the `.class` files into a JAR file. + * * 3. Convert the JAR into DEX format for Android. + * * 4. Package DEX files and resources into an APK. + * * 5. Optimize the APK using zipalign. + * * 6. Sign the APK for distribution. * - * For detailed information, refer to Mill's [documentation](https://com-lihaoyi.github.io/mill), - * and the [Android developer guide](https://developer.android.com/studio). + * For detailed information, refer to Mill's Documentation [[https://com-lihaoyi.github.io/mill]], + * and the Android Dcoumentation [[https://developer.android.com/studio]]. */ trait AndroidAppModule extends AndroidSdkModule with JavaModule { @@ -47,7 +53,9 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Step 1: Compile Java source files to `.class` files. * * This method: + * * - Ensures the Android SDK is installed. + * * - Compiles all `.java` files in `src/main/java` to `.class` files stored in `obj/` directory. * * @return A `PathRef` to the directory containing the compiled `.class` files. @@ -77,6 +85,7 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Step 2: Package `.class` files into a JAR file. * * This method: + * * - Converts the compiled `.class` files into a JAR file using the `d8` tool. * * @return A `PathRef` to the generated JAR file. @@ -105,6 +114,7 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Step 3: Convert the JAR file into a DEX file. * * This method: + * * - Uses the `d8` tool to convert the JAR file into DEX format, required for Android apps. * * @return A `PathRef` to the generated DEX file. @@ -128,6 +138,7 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Step 4: Package the DEX file into an unsigned APK. * * This method: + * * - Uses the `aapt` tool to create an APK file that includes the DEX file and resources. * * @return A `PathRef` to the unsigned APK file. @@ -159,6 +170,7 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Step 5: Optimize the APK using zipalign. * * This method: + * * - Takes the unsigned APK and optimizes it for better performance on Android devices. * * @return A `PathRef` to the aligned APK file. @@ -187,6 +199,7 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Step 6: Sign the APK using a keystore. * * This method: + * * - Signs the aligned APK with a keystore. If the keystore does not exist, it generates one. * * @return A `PathRef` to the signed APK file. @@ -223,6 +236,7 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { * Creates a keystore for signing APKs if it doesn't already exist. * * This method: + * * - Generates a keystore file using the `keytool` command. * * @return A `PathRef` to the keystore file. diff --git a/scalalib/src/mill/javalib/android/AndroidSdkModule.scala b/scalalib/src/mill/javalib/android/AndroidSdkModule.scala index f317d8d9f35..425f2878819 100644 --- a/scalalib/src/mill/javalib/android/AndroidSdkModule.scala +++ b/scalalib/src/mill/javalib/android/AndroidSdkModule.scala @@ -12,7 +12,8 @@ import mill.define._ * It simplifies the process of configuring the Android development environment, * making it easier to build and package Android applications. * - * For more, refer to the [Android SDK documentation](https://developer.android.com/studio). + * For detailed information, refer to Mill's Documentation [[https://com-lihaoyi.github.io/mill]], + * and the Android Dcoumentation [[https://developer.android.com/studio]]. */ trait AndroidSdkModule extends Module { @@ -129,11 +130,12 @@ trait AndroidSdkModule extends Module { * * This method: * - Downloads the SDK command-line tools from the specified URL. + * * - Extracts the downloaded zip file into the specified SDK directory. + * * - Accepts the SDK licenses required for use. - * - Installs essential components such as platform-tools, build-tools and platforms. * - * @throws Exception if any step fails during installation. + * - Installs essential components such as platform-tools, build-tools and platforms. * * @see [[SdkUrl]] * @see [[toolsDirectory]] From 6ac8aaa12c09672aeadfad3b851c0e94791db9ae Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Fri, 4 Oct 2024 23:31:48 +0530 Subject: [PATCH 3/4] Fixes:#3550 ; Final Review Changes: Fixed --- .../ROOT/pages/javalib/android-examples.adoc | 57 +-- .../javalib/android/1-hello-world/build.mill | 79 +++-- .../javalib/android/AndroidAppModule.scala | 333 ++++++++++-------- .../javalib/android/AndroidSdkModule.scala | 171 ++++----- 4 files changed, 339 insertions(+), 301 deletions(-) diff --git a/docs/modules/ROOT/pages/javalib/android-examples.adoc b/docs/modules/ROOT/pages/javalib/android-examples.adoc index 8ee4af33de8..4e7323bbca3 100644 --- a/docs/modules/ROOT/pages/javalib/android-examples.adoc +++ b/docs/modules/ROOT/pages/javalib/android-examples.adoc @@ -9,40 +9,55 @@ gtag('config', 'AW-16649289906'); This page provides examples of using Mill as a build tool for Android applications. Each example highlights a specific aspect of Mill, allowing you to configure, test, and build Android apps efficiently. -By the end, you will understand how Mill can manage Android project structures, compile modules, and handle packaging. +By the end of this guide, you will understand how Mill can manage Android project structures, compile modules, and handle APK packaging. -For detailed information, refer to Mill's https://com-lihaoyi.github.io/mill[documentation], -and the https://developer.android.com/studio[Android developer guide]. +For detailed information, refer to Mill's https://com-lihaoyi.github.io/mill[official documentation], +and the https://developer.android.com/studio[Android Developer Guide]. -The following APIs are relevant to this documentation: +== Relevant Modules -* {mill-doc-url}/api/latest/mill/scalalib/AndroidSdkModule.html[`mill.scalalib.AndroidSdkModule`] -* {mill-doc-url}/api/latest/mill/javalib/android/AndroidAppModule.html[`mill.scalalib.AndroidAppModule`] -* {mill-doc-url}/api/latest/mill/scalalib/JavaModule.html[`mill.scalalib.JavaModule`] +These are the main Mill Modules that are relevant for building Android apps: + +* {mill-doc-url}/api/latest/mill/scalalib/AndroidSdkModule.html[`mill.scalalib.AndroidSdkModule`]: Handles Android SDK management and tools. +* {mill-doc-url}/api/latest/mill/javalib/android/AndroidAppModule.html[`mill.javalib.android.AndroidAppModule`]: Provides a framework for building Android applications. +* {mill-doc-url}/api/latest/mill/scalalib/JavaModule.html[`mill.scalalib.JavaModule`]: General Java build tasks like compiling Java code and creating JAR files. == Simple Android Hello World Application include::partial$example/javalib/android/1-hello-world.adoc[] -Curious about what's happening behind the scenes, -here is some information for `AndroidSdkModule` and `AndroidAppModule` +This example demonstrates how to create a basic "Hello World" Android application using the Mill build tool. It outlines the minimum setup required to compile Java code, package it into an APK, and run the app on an Android device. + +== Understanding `AndroidSdkModule` and `AndroidAppModule` + +The two main modules you need to understand when building Android apps with Mill are `AndroidSdkModule` and `AndroidAppModule`. `AndroidSdkModule`: -* This installs Android SDK Tool. +* This module manages the installation and configuration of the Android SDK, which includes tools like `aapt`, `d8`, `zipalign`, and `apksigner`. These tools are crucial for compiling, packaging, and signing Android applications. `AndroidAppModule`: +This module provides the step-by-step workflow for building an Android app. It handles everything from compiling the code to generating a signed APK for distribution. + +1. **Compiling Java code**: The module compiles your Java code into `.class` files, which is the first step in creating an Android app. +2. **Packaging into JAR**: It then packages the compiled `.class` files into a JAR file, which is necessary before converting to Android's format. +3. **Converting to DEX format**: The JAR file is converted into DEX format, which is the executable format for Android applications. +4. **Creating an APK**: The DEX files and Android resources (like layouts and strings) are packaged together into an APK file, which is the installable file for Android devices. +5. **Optimizing with zipalign**: The APK is optimized using `zipalign` to ensure better performance on Android devices. +6. **Signing the APK**: Finally, the APK is signed with a digital signature, allowing it to be distributed and installed on Android devices. + +== Testing Your Application + +You can perform manual testing on your Android APK to ensure it functions as expected. The Mill team is working on automating testing procedures for Android applications, which will be shared in future updates. + +== Why Use Mill for Android Development? + +Mill provides several advantages over other build tools, including: + +* **Efficiency**: Mill's build process is fast and optimized for minimal configuration, which speeds up the Android app creation process. -* 1. Compile Java code into `.class` files. -* 2. Package the `.class` files into a JAR file. -* 3. Convert the JAR into DEX format for Android. -* 4. Package DEX files and resources into an APK. -* 5. Optimize the APK using zipalign -* 6. Sign the APK for distribution. +* **Modularity**: Mill allows you to easily extend and customize your builds by leveraging its modular design. -Users can Perform Manual Testing to insure Proper Functioning of the Application and Automatic -Testing Procedure is still under Observance once Approved, will be shared. +* **Scalability**: Mill can scale well for larger projects while maintaining simplicity in configuration. -Mill Build Tool already have Advantages over the other Build Tools and -having the support for Android Application with Mill Build Tool Provdies Efficieny and -Optimisation to Android Application Creation Process. \ No newline at end of file +By using Mill, you can streamline your Android app development, saving time and reducing complexity. diff --git a/example/javalib/android/1-hello-world/build.mill b/example/javalib/android/1-hello-world/build.mill index 7de7ad37374..b3833fbf974 100644 --- a/example/javalib/android/1-hello-world/build.mill +++ b/example/javalib/android/1-hello-world/build.mill @@ -1,46 +1,55 @@ -// This section demonstrates how to set up a basic Android app structure -// with `AndroidSdkModule.scala` and `AndroidAppModule.scala`, utilizing traits for scalability. - -// Basically, `AndroidAppModule` extends `AndroidSdkModule` so we need to call only AndroidAppModule this will -// insure that all tasks are automatically done for AndroidSdkModule, making user expeience easy - -// This Example demonstrates a simple "Hello World" -// Android application built using the https://www.lihaoyi.com/mill[Mill build tool]. +// This section sets up a basic Android project using Mill. +// We utilize `AndroidAppModule` and `AndroidSdkModule` to streamline the process of +// building an Android application with minimal configuration. +// +// By extending `AndroidAppModule`, we inherit all Android-related tasks such as +// resource generation, APK building, DEX conversion, and APK signing. +// Additionally, `AndroidSdkModule` is embedded, making SDK management seamless. //// SNIPPET:BUILD package build import mill._ -import mill.javalib.android.AndroidAppModule +import mill.javalib.android.{AndroidAppModule, AndroidSdkModule} + +object `package` extends RootModule with AndroidAppModule { -object App extends AndroidAppModule { + // Create and configure an Android SDK module to manage Android SDK paths and tools. + object sdkModule extends AndroidSdkModule - // Default project root to be one level up from the current millSourcePath - def projectRoot: T[os.Path] = T { - os.Path(millSourcePath.toString.replace("App", "")) - } - // The name of the Android application, default is "HelloWorld". - def appName: T[String] = T { "HelloWorld" } + // Override the `androidSdkModule` to link to our custom `sdkModule`, which provides + // Android SDK toolchain (e.g., `aapt`, `d8`, `zipalign`, `apksigner`). + override def androidSdkModule: AndroidSdkModule = sdkModule + + // Define the Android application name. Defaults to "HelloWorld", but can be customized + // by overriding this value in the project. + def androidAppName: T[String] = T { "HelloWorld" } } ////SNIPPET:END + /** Usage -> ./mill App.createApp +> ./mill androidApp */ -// This is a basic Mill build for creating Simple `Hello-World` Android Application -// which extends `AndroidAppModule` for all Andorid Application related tasks, -// here we can alter the default values: -// -// `projectRoot`(Which Contains All Files required for Android Application Creation). +// This command triggers the build process, which installs the Android Setup, compiles the Java +// code, generates Android resources, converts Java bytecode to DEX format, packages everything +// into an APK, optimizes the APK using `zipalign`, and finally signs it. // -// `appName` (responsible For Final Application Name). +// This Mill build configuration is designed to build a simple "Hello World" Android application. +// By extending `AndroidAppModule`, we leverage its predefined Android build tasks, ensuring that +// all necessary steps (resource generation, APK creation, and signing) are executed automatically. // -// User can Change these values according to their need and the Project Folder Structure -// for this would look something like this: +// ### Customizable Fields: +// +// - **`androidAppName`**: This defines the name of the final APK file. By default, it's set to +// "HelloWorld.apk", but you can modify this value based on your project requirements. +// +// ### Project Structure: +// The project follows the standard Android app layout. Below is a typical project folder structure: // // ---- // . @@ -53,12 +62,20 @@ object App extends AndroidAppModule { // └── helloworld // └── app // └── MainActivity.java -// // ---- +// +// ### Mill Modules Overview: +// +// This project relies on three main modules: +// +// 1. **`AndroidSdkModule`**: This module manages all Android SDK interactions, including +// tools like `aapt` for resource packaging, `d8` for bytecode conversion, and `apksigner` for signing APKs. // -//// SNIPPET:Modules +// 2. **`JavaModule`**: This provides Java compilation tasks, including class file generation +// and creating JAR files, which are later converted to DEX format for Android. // -// This example project uses `AndroidAppModule` and this Module further depends on two Modules: -// * `AndroidSdkModule`(For Installing Andorid SDK and Tools Required to create Android Application) -// * `JavaModule` (For Using Java Commands for Compiling, Creating Jars and common java tasks) -// all the code works in a flow from installing Android SDK to creating Android Application. +// 3. **`AndroidAppModule`**: This module provides the step-by-step workflow for building an Android app. +// It handles everything from compiling the code to generating a signed APK for distribution. +// +// The combination of these modules allows us to automate the entire build process, +// from compiling Java code to producing a signed APK ready for distribution. diff --git a/scalalib/src/mill/javalib/android/AndroidAppModule.scala b/scalalib/src/mill/javalib/android/AndroidAppModule.scala index 51e1943170f..eba55a65e9e 100644 --- a/scalalib/src/mill/javalib/android/AndroidAppModule.scala +++ b/scalalib/src/mill/javalib/android/AndroidAppModule.scala @@ -3,248 +3,305 @@ package mill.javalib.android import mill._ import mill.api.PathRef import mill.scalalib.JavaModule -import mill.javalib.android.AndroidSdkModule -import mill.util.Jvm /** - * Trait for building Android applications using Mill, - * this extends [[AndroidSdkModule]] for Android SDK related tasks, - * and [[JavaModule]] for Java related tasks. + * Trait for building Android applications using the Mill build tool. * - * This trait outlines the steps necessary to build an Android application: + * This trait defines all the necessary steps for building an Android app from Java sources, + * integrating both Android-specific tasks and generic Java tasks by extending the + * [[AndroidSdkModule]] (for Android SDK interactions) and [[JavaModule]] (for standard Java tasks). * - * 1. Compile Java code into `.class` files. + * It provides a structured way to handle various steps in the Android app build process, + * including compiling Java sources, creating DEX files, generating resources, packaging + * APKs, optimizing, and signing APKs. * - * 2. Package the `.class` files into a JAR file. + * The overall build process includes: * - * 3. Convert the JAR into DEX format for Android. + * 1. Compilation: Compiles the Java source code into `.class` files. * - * 4. Package DEX files and resources into an APK. + * 2. Packaging: Packages the compiled `.class` files into a JAR file. * - * 5. Optimize the APK using zipalign. + * 3. Conversion to DEX: Converts the JAR file into DEX format (Android's runtime format). * - * 6. Sign the APK for distribution. + * 4. APK Creation: Packages the DEX files and other resources into an APK. * - * For detailed information, refer to Mill's Documentation [[https://com-lihaoyi.github.io/mill]], - * and the Android Dcoumentation [[https://developer.android.com/studio]]. + * 5. Optimization: Optimizes the APK for better performance using zipalign. + * + * 6. Signing: Signs the APK with a digital signature for distribution. + * + * By following these steps, developers can automate their Android application build + * workflow using Mill and the Android SDK. + * + * Resources for further reading: + * + * [[https://com-lihaoyi.github.io/mill Mill Documentation]] + * + * [[https://developer.android.com/studio Android Studio Documentation]] */ -trait AndroidAppModule extends AndroidSdkModule with JavaModule { +trait AndroidAppModule extends JavaModule { /** - * Path where the Project related Files will live. + * Abstract method to provide access to the Android SDK configuration. + * + * The `AndroidSdkModule` provides access to important SDK tools such as `aapt` (for packaging resources), + * `d8` (for compiling to DEX), `zipalign` (for APK optimization), and `apksigner` (for APK signing). * - * @return A `PathRef` representing project directory. + * This method must be implemented by the concrete class to specify the SDK paths. + * + * @return The Android SDK module that is used across the project. */ - def projectRoot: T[os.Path] = T { - os.Path(millSourcePath.toString.replace( - "App", - "" - )) // Get the parent directory of millSourcePath - } + def androidSdkModule: AndroidSdkModule /** - * App Name for the Application default is HelloWorld. + * Defines the name of the Android application. + * + * The name is used as part of the APK file name (e.g., `HelloWorld.apk`), and also + * helps distinguish different builds. It defaults to "HelloWorld", but can be + * overridden for custom app names. * - * @return A string representing the platform version. + * @return The name of the Android application as a String. */ - def appName: T[String] = T { "HelloWorld" } + def androidAppName: T[String] = Task { "HelloWorld" } /** - * Step 1: Compile Java source files to `.class` files. + * Generates the Android resources (such as layouts, strings, and other assets) needed for the application. * - * This method: + * This method uses the Android `aapt` tool to compile resources specified in the project's `AndroidManifest.xml` + * and any additional resource directories. It creates the necessary R.java files and other compiled resources + * for Android. These generated resources are crucial for the app to function correctly on Android devices. * - * - Ensures the Android SDK is installed. + * For more details on the aapt tool, refer to: + * [[https://developer.android.com/tools/aapt2 aapt Documentation]] * - * - Compiles all `.java` files in `src/main/java` to `.class` files stored in `obj/` directory. + * @return A `PathRef` pointing to the directory where the generated resources are stored. + */ + def androidResources: T[PathRef] = Task { + val genDir: os.Path = T.dest // Directory to store generated resources. + + os.call(Seq( + androidSdkModule.aaptPath().path.toString, // Call aapt tool + "package", + "-f", + "-m", + "-J", + genDir.toString, // Generate R.java files + "-M", + (millSourcePath / "src/main/AndroidManifest.xml").toString, // Use AndroidManifest.xml + "-I", + androidSdkModule.androidJarPath().path.toString // Include Android SDK JAR + )) + + PathRef(genDir) + } + + /** + * Adds the Android SDK JAR file to the classpath during the compilation process. + * + * This method makes sure that the Android framework classes (like `android.view.View`) + * are available when compiling the Java sources. * - * @return A `PathRef` to the directory containing the compiled `.class` files. + * For more Information please check: JavaModule [[unmanagedClasspath]] * - * @see [[createJar]] + * @return A collection of paths that make up the unmanaged classpath, which includes the Android JAR. */ - def compileJava: T[PathRef] = T { - installAndroidSdk() // Step 1: Install the Android SDK if not already done. - val outputDir = T.dest / "obj" // Directory to store compiled class files. - - os.call( - Seq( - Jvm.jdkTool("javac"), // Use the Java compiler - "-classpath", - androidJarPath().path.toString, // Include Android framework classes - "-d", - outputDir.toString // Specify output directory for class files - ) ++ os.walk(projectRoot() / "src/main/java").filter(_.ext == "java").map( - _.toString - ) // Get all Java source files - ) + def unmanagedClasspath: T[Agg[PathRef]] = Task { + Agg(androidSdkModule.androidJarPath()) + } - PathRef(outputDir) // Return the path to compiled class files. + /** + * Combines standard Java source directories with additional sources generated by + * the Android resource generation step. + * + * This method ensures that generated files like `R.java` (which contain references to resources) + * are included in the source set and compiled correctly. + * + * For more Information please check: JavaModule [[generatedSources]] + * + * @return A sequence of source paths including both regular sources and generated ones. + */ + def generatedSources: T[Seq[PathRef]] = Task { + super.generatedSources() ++ Seq(androidResources()) } /** - * Step 2: Package `.class` files into a JAR file. + * Packages the compiled Java `.class` files into a JAR file using the D8 tool. + * + * The D8 compiler is used here to package and optimize the Java bytecode into a format + * suitable for Android (DEX). D8 converts the Java `.class` files into a jar file which is + * suitable for DEX (Dalvik Executable) format and is required for Android runtime. + * + * Why use D8 instead of standard JAR packaging: + * + * D8 Compiler: Converts Java bytecode into jar(with DEX support) files, optimized for Android. + * + * - Provides performance optimizations for Android devices, such as desugaring, + * which enables newer Java language features. * - * This method: + * - Results in smaller APK sizes, leading to faster and more efficient apps. * - * - Converts the compiled `.class` files into a JAR file using the `d8` tool. + * Standard JAR Command: Simply packages `.class` files without applying Android-specific + * optimizations. It lacks the performance and size benefits provided by D8. * - * @return A `PathRef` to the generated JAR file. + * Conclusion: Using D8 for creating JARs ensures smaller, faster applications optimized + * for the Android ecosystem. * - * @see [[compileJava]] - * @see [[createDex]] + * For more details on the d8 tool, refer to: + * [[https://developer.android.com/tools/d8 d8 Documentation]] + * + * @return A `PathRef` pointing to the generated JAR file. */ - def createJar: T[PathRef] = T { - val jarFile = T.dest / "my_classes.jar" // Specify output JAR file name. + def androidJar: T[PathRef] = Task { + val jarFile: os.Path = T.dest / "my_classes.jar" os.call( Seq( - d8Path().path.toString, // Path to the D8 tool + androidSdkModule.d8Path().path.toString, // Call d8 tool "--output", jarFile.toString, // Output JAR file - "--no-desugaring" // Do not apply desugaring - ) ++ os.walk(compileJava().path).filter(_.ext == "class").map( + "--no-desugaring" // Disable desugaring + ) ++ os.walk(compile().classes.path).filter(_.ext == "class").map( _.toString - ) // Get compiled class files from compileJava + ) // Get class files ) - PathRef(jarFile) // Return the path to the created JAR file. + PathRef(jarFile) } /** - * Step 3: Convert the JAR file into a DEX file. - * - * This method: + * Converts the generated JAR file into a DEX file using the `d8` tool. * - * - Uses the `d8` tool to convert the JAR file into DEX format, required for Android apps. + * DEX (Dalvik Executable) files are the binary format for Android applications. This method + * takes the JAR file created in the previous step and converts it into one or more DEX files. * - * @return A `PathRef` to the generated DEX file. + * This step is crucial because Android devices do not execute `.class` files directly; + * they require the code to be in DEX format. * - * @see [[createJar]] + * @return A `PathRef` pointing to the folder containing the generated DEX files. */ - def createDex: T[PathRef] = T { - val dexOutputDir = T.dest // Directory to store DEX files. + def androidDex: T[PathRef] = Task { + val dexOutputDir: os.Path = T.dest os.call( - Seq(d8Path().path.toString, "--output", dexOutputDir.toString) ++ Seq( - createJar().path.toString, // Use the JAR file from createJar - androidJarPath().path.toString // Include Android framework classes - ) + Seq(androidSdkModule.d8Path().path.toString, "--output", dexOutputDir.toString) ++ + Seq( + androidJar().path.toString, // Use the JAR file from the previous step + androidSdkModule.androidJarPath().path.toString // Include Android framework classes + ) ) - PathRef(dexOutputDir) // Return the path to the generated DEX file. + PathRef(dexOutputDir) } /** - * Step 4: Package the DEX file into an unsigned APK. + * Packages the DEX files and Android resources into an unsigned APK using the `aapt` tool. * - * This method: + * The `aapt` tool takes the DEX files (compiled code) and resources (such as layouts and assets), + * and packages them into an APK (Android Package) file. This APK file is unsigned and requires + * further processing to be distributed. * - * - Uses the `aapt` tool to create an APK file that includes the DEX file and resources. - * - * @return A `PathRef` to the unsigned APK file. - * - * @see [[createDex]] + * @return A `PathRef` pointing to the unsigned APK file. */ - def createApk: T[PathRef] = T { - val unsignedApk = - T.dest / s"${appName().toString}.unsigned.apk" // Specify output APK file name. + def androidApk: T[PathRef] = Task { + val unsignedApk: os.Path = T.dest / s"${androidAppName().toString}.unsigned.apk" os.call( Seq( - aaptPath().path.toString, - "package", // Command to package APK - "-f", // Force overwrite + androidSdkModule.aaptPath().path.toString, + "package", + "-f", "-M", - (projectRoot() / "src/main/AndroidManifest.xml").toString, // Path to the AndroidManifest.xml + (millSourcePath / "src/main/AndroidManifest.xml").toString, // Path to AndroidManifest.xml "-I", - androidJarPath().path.toString, // Include Android framework resources + androidSdkModule.androidJarPath().path.toString, // Include Android JAR "-F", - unsignedApk.toString // Specify output APK file - ) ++ Seq(createDex().path.toString) // Include the DEX file from createDex + unsignedApk.toString // Output APK + ) ++ Seq(androidDex().path.toString) // Include DEX files ) - PathRef(unsignedApk) // Return the path to the unsigned APK. + PathRef(unsignedApk) } /** - * Step 5: Optimize the APK using zipalign. + * Optimizes the APK using the `zipalign` tool for better performance. * - * This method: + * The `zipalign` tool ensures that all uncompressed data in the APK is aligned on a 4-byte boundary. + * This is required for better performance on Android devices. This step is done after the APK + * is created but before it is signed. * - * - Takes the unsigned APK and optimizes it for better performance on Android devices. + * For more details on the zipalign tool, refer to: + * [[https://developer.android.com/tools/zipalign zipalign Documentation]] * - * @return A `PathRef` to the aligned APK file. - * - * @see [[createApk]] + * @return A `PathRef` pointing to the aligned APK file. */ - def alignApk: T[PathRef] = T { - val alignedApk = - T.dest / s"${appName().toString}.aligned.apk" // Specify output aligned APK file name. + def androidAlignApk: T[PathRef] = Task { + val alignedApk: os.Path = T.dest / s"${androidAppName().toString}.aligned.apk" os.call( Seq( - zipalignPath().path.toString, // Path to the zipalign tool + androidSdkModule.zipalignPath().path.toString, // Call zipalign tool "-f", "-p", - "4", // Force overwrite and align with a page size of 4 - createApk().path.toString, // Use the unsigned APK from createApk - alignedApk.toString // Specify output aligned APK file + "4", // Force overwrite, align with 4-byte boundary + androidApk().path.toString, // Use the unsigned APK + alignedApk.toString // Output aligned APK ) ) - PathRef(alignedApk) // Return the path to the aligned APK. + PathRef(alignedApk) } /** - * Step 6: Sign the APK using a keystore. + * Signs the APK using a keystore to generate a final, distributable APK. * - * This method: + * 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. * - * - Signs the aligned APK with a keystore. If the keystore does not exist, it generates one. + * If no keystore is available, a new one is generated using the `keytool` utility. * - * @return A `PathRef` to the signed APK file. + * For more details on the apksigner tool, refer to: + * [[https://developer.android.com/tools/apksigner apksigner Documentation]] * - * @see [[alignApk]] - * @see [[createKeystore]] + * @return A `PathRef` pointing to the signed APK. */ - def createApp: T[PathRef] = T { - val signedApk = - projectRoot() / s"${appName().toString}.apk" // Specify output signed APK file name. + def androidApp: T[PathRef] = Task { + val signedApk: os.Path = millSourcePath / s"${androidAppName().toString}.apk" os.call( Seq( - apksignerPath().path.toString, - "sign", // Command to sign APK + androidSdkModule.apksignerPath().path.toString, + "sign", // Call apksigner tool "--ks", - createKeystore().path.toString, // Use the keystore from createKeystore + androidKeystore().path.toString, // Path to keystore "--ks-key-alias", - "androidkey", // Alias for the key + "androidkey", // Key alias "--ks-pass", "pass:android", // Keystore password "--key-pass", "pass:android", // Key password "--out", - signedApk.toString, // Specify output signed APK file - alignApk().path.toString // Use the aligned APK from alignApk + signedApk.toString, // Output signed APK + androidAlignApk().path.toString // Use aligned APK ) ) - PathRef(signedApk) // Return the path to the signed APK. + PathRef(signedApk) } /** - * Creates a keystore for signing APKs if it doesn't already exist. - * - * This method: + * Generates a new keystore file if it does not exist. * - * - Generates a keystore file using the `keytool` command. + * A keystore is required to sign the APK for distribution. This method checks if a keystore + * exists, and if not, generates a new one using the `keytool` utility. The keystore holds + * the cryptographic keys used to sign the APK. * - * @return A `PathRef` to the keystore file. + * For more details on the keytool utility, refer to: + * [[https://docs.oracle.com/javase/8/docs/technotes/tools/windows/keytool.html keytool Documentation]] * - * @see [[createApp]] + * @return A `PathRef` pointing to the keystore file. */ - def createKeystore: T[PathRef] = T { - val keystoreFile = T.dest / "keystore.jks" // Specify keystore file name. + def androidKeystore: T[PathRef] = Task { + val keystoreFile: os.Path = T.dest / "keystore.jks" if (!os.exists(keystoreFile)) { os.call( @@ -254,23 +311,23 @@ trait AndroidAppModule extends AndroidSdkModule with JavaModule { "-keystore", keystoreFile.toString, // Generate keystore "-alias", - "androidkey", // Key alias + "androidkey", // Alias for key in the keystore "-dname", - "CN=MILL, OU=MILL, O=MILL, L=MILL, S=MILL, C=IN", // Distinguished name + "CN=MILL, OU=MILL, O=MILL, L=MILL, S=MILL, C=IN", // Key details "-validity", - "10000", // Validity period in days + "10000", // Valid for 10,000 days "-keyalg", - "RSA", // Key algorithm + "RSA", "-keysize", - "2048", // Key size + "2048", // RSA encryption, 2048-bit key "-storepass", - "android", // Keystore password + "android", "-keypass", - "android" // Key password + "android" // Passwords ) ) } - PathRef(keystoreFile) // Return the path to the keystore file. + PathRef(keystoreFile) } } diff --git a/scalalib/src/mill/javalib/android/AndroidSdkModule.scala b/scalalib/src/mill/javalib/android/AndroidSdkModule.scala index 425f2878819..3b667091d88 100644 --- a/scalalib/src/mill/javalib/android/AndroidSdkModule.scala +++ b/scalalib/src/mill/javalib/android/AndroidSdkModule.scala @@ -1,175 +1,124 @@ package mill.javalib.android import mill._ -import mill.define._ /** - * Trait for managing the Android SDK in a Mill build. + * Trait for managing the Android SDK in a Mill build system. * - * This trait provides methods for downloading and setting up the Android SDK, - * build tools, and other resources required for Android development. + * This trait offers utility methods for automating the download, installation, + * and configuration of the Android SDK, build tools, and other essential + * components necessary for Android development. It facilitates setting up + * an Android development environment, streamlining the process of building, + * compiling, and packaging Android applications in a Mill project. * - * It simplifies the process of configuring the Android development environment, - * making it easier to build and package Android applications. + * The trait handles tasks such as fetching the Android SDK, managing versions + * of the build tools, providing paths to necessary executables, and setting up + * resources required for compiling, optimizing, and signing Android apps. * - * For detailed information, refer to Mill's Documentation [[https://com-lihaoyi.github.io/mill]], - * and the Android Dcoumentation [[https://developer.android.com/studio]]. + * For more information, refer to Mill's [[https://com-lihaoyi.github.io/mill documentation]], + * and the official Android [[https://developer.android.com/studio documentation]]. */ trait AndroidSdkModule extends Module { /** - * URL to download the Android SDK command-line tools. - * - * @return A string representing the URL for the SDK tools. + * Provides the URL to download the Android SDK command-line tools. */ - def SdkUrl: T[String] = T { + def sdkUrl: T[String] = Task { "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" } /** - * Version of Android build tools. - * - * @return A string representing the version of the build tools. - */ - def BuildToolsVersion: T[String] = T { "35.0.0" } - - /** - * Version of Android platform (e.g., Android API level). - * - * @return A string representing the platform version. + * Specifies the version of the Android build tools to be used. */ - def PlatformVersion: T[String] = T { "android-35" } + def buildToolsVersion: T[String] = Task { "35.0.0" } /** - * Directory name for the Android command-line tools. - * - * @return A string representing the directory name for the tools. + * Specifies the Android platform version (e.g., Android API level). */ - def ToolsDirName: T[String] = T { "cmdline-tools" } + def platformsVersion: T[String] = Task { "android-35" } /** - * Name of the zip file containing the SDK tools. - * - * @return A string representing the zip file name. + * Provides the path to the `android.jar` file, necessary for compiling Android apps. */ - def ZipFileName: T[String] = T { "commandlinetools.zip" } - - /** - * Path where the Android SDK will be installed. - * - * @return A `PathRef` representing the SDK installation directory. - */ - def sdkDirectory: T[PathRef] = T { PathRef(millSourcePath / "android-sdk") } - - /** - * Path to the Android SDK command-line tools directory. - * - * @return A `PathRef` representing the command-line tools directory. - */ - def toolsDirectory: T[PathRef] = T { PathRef(sdkDirectory().path / ToolsDirName().toString) } + def androidJarPath: T[PathRef] = Task { + PathRef(installAndroidSdk().path / "platforms" / platformsVersion().toString / "android.jar") + } /** - * Path to the Android build tools based on the selected version. - * - * @return A `PathRef` representing the build tools directory. - * - * @see [[BuildToolsVersion]] + * Provides path to the Android build tools for the selected version. */ def buildToolsPath: T[PathRef] = - T { PathRef(sdkDirectory().path / "build-tools" / BuildToolsVersion().toString) } + Task { PathRef(installAndroidSdk().path / "build-tools" / buildToolsVersion().toString) } /** - * Path to `android.jar`, required for compiling Android apps. - * - * @return A `PathRef` representing the path to `android.jar`. - * - * @see [[PlatformVersion]] - */ - def androidJarPath: T[PathRef] = - T { PathRef(sdkDirectory().path / "platforms" / PlatformVersion().toString / "android.jar") } - - /** - * Path to the D8 Dex compiler, used to convert Java bytecode to Dalvik bytecode. - * - * @return A `PathRef` representing the path to the D8 compiler. - * - * @see [[buildToolsPath]] + * Provides path to D8 Dex compiler, used for converting Java bytecode into Dalvik bytecode. */ - def d8Path: T[PathRef] = T { PathRef(buildToolsPath().path / "d8") } + def d8Path: T[PathRef] = Task { + PathRef(buildToolsPath().path / "d8") + } /** - * Path to the Android Asset Packaging Tool (AAPT) for handling resources and packaging APKs. - * - * @return A `PathRef` representing the path to the AAPT tool. - * - * @see [[buildToolsPath]] + * Provides the path to AAPT, used for resource handling and APK packaging. */ - def aaptPath: T[PathRef] = T { PathRef(buildToolsPath().path / "aapt") } + def aaptPath: T[PathRef] = Task { + PathRef(buildToolsPath().path / "aapt") + } /** - * Path to Zipalign, used to optimize APKs. - * - * @return A `PathRef` representing the path to the zipalign tool. - * - * @see [[buildToolsPath]] + * Provides the path to the Zipalign tool, which optimizes APK files by aligning their data. */ - def zipalignPath: T[PathRef] = T { PathRef(buildToolsPath().path / "zipalign") } + def zipalignPath: T[PathRef] = Task { + PathRef(buildToolsPath().path / "zipalign") + } /** - * Path to the APK signer tool, used to sign APKs. - * - * @return A `PathRef` representing the path to the APK signer tool. - * - * @see [[buildToolsPath]] + * Provides the path to the APK signer tool, used to digitally sign APKs. */ - def apksignerPath: T[PathRef] = T { PathRef(buildToolsPath().path / "apksigner") } + def apksignerPath: T[PathRef] = Task { + PathRef(buildToolsPath().path / "apksigner") + } /** - * Installs the Android SDK by downloading the tools, extracting them, - * accepting licenses, and installing necessary components like platform and build tools. + * Installs the Android SDK by performing the following actions: * - * This method: * - Downloads the SDK command-line tools from the specified URL. * - * - Extracts the downloaded zip file into the specified SDK directory. + * - Extracts the downloaded zip file into the SDK directory. * - * - Accepts the SDK licenses required for use. + * - Accepts the required SDK licenses. * - * - Installs essential components such as platform-tools, build-tools and platforms. + * - Installs essential SDK components such as platform-tools, build-tools, and Android platforms. * - * @see [[SdkUrl]] - * @see [[toolsDirectory]] - * @see [[sdkDirectory]] - * @see [[BuildToolsVersion]] - * @see [[PlatformVersion]] + * For more details on the sdkmanager tool, refer to: + * [[https://developer.android.com/tools/sdkmanager sdkmanager Documentation]] + * + * @return A task containing a `PathRef` pointing to the installed SDK directory. */ - def installAndroidSdk: T[Unit] = T { - val zipFilePath: os.Path = sdkDirectory().path / ZipFileName().toString - val sdkManagerPath: os.Path = toolsDirectory().path / "bin" / "sdkmanager" - - // Create SDK directory if it doesn't exist - os.makeDir.all(sdkDirectory().path) + def installAndroidSdk: T[PathRef] = Task { + val zipFilePath: os.Path = T.dest / "commandlinetools.zip" + val sdkManagerPath: os.Path = T.dest / "cmdline-tools/bin/sdkmanager" // Download SDK command-line tools - os.write(zipFilePath, requests.get(SdkUrl().toString).bytes) + os.write(zipFilePath, requests.get(sdkUrl().toString).bytes) - // Extract the zip into the SDK directory - os.call(Seq("unzip", zipFilePath.toString, "-d", sdkDirectory().path.toString)) + // Extract the downloaded SDK tools into the destination directory + os.call(Seq("unzip", zipFilePath.toString, "-d", T.dest.toString)) - // Accept SDK licenses + // Automatically accept the SDK licenses os.call(Seq( "bash", "-c", - s"yes | $sdkManagerPath --licenses --sdk_root=${sdkDirectory().path}" + s"yes | $sdkManagerPath --licenses --sdk_root=${T.dest}" )) - // Install platform-tools, build-tools, and platform + // Install platform-tools, build-tools, and the Android platform os.call(Seq( sdkManagerPath.toString, - s"--sdk_root=${sdkDirectory().path}", + s"--sdk_root=${T.dest}", "platform-tools", - s"build-tools;${BuildToolsVersion().toString}", - s"platforms;${PlatformVersion().toString}" + s"build-tools;${buildToolsVersion().toString}", + s"platforms;${platformsVersion().toString}" )) + PathRef(T.dest) } } From 84898234c38194097145752cbe3f56cc81bcfd60 Mon Sep 17 00:00:00 2001 From: himanshumahajan138 Date: Sat, 5 Oct 2024 18:49:51 +0530 Subject: [PATCH 4/4] Fixes:#3550; Final Changes Fixed --- .../javalib/android/1-hello-world/build.mill | 16 ++-------- .../javalib/android/AndroidAppModule.scala | 29 ++++++------------- 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/example/javalib/android/1-hello-world/build.mill b/example/javalib/android/1-hello-world/build.mill index b3833fbf974..63b06f1e87d 100644 --- a/example/javalib/android/1-hello-world/build.mill +++ b/example/javalib/android/1-hello-world/build.mill @@ -15,15 +15,8 @@ import mill.javalib.android.{AndroidAppModule, AndroidSdkModule} object `package` extends RootModule with AndroidAppModule { // Create and configure an Android SDK module to manage Android SDK paths and tools. - object sdkModule extends AndroidSdkModule + object androidSdkModule extends AndroidSdkModule - // Override the `androidSdkModule` to link to our custom `sdkModule`, which provides - // Android SDK toolchain (e.g., `aapt`, `d8`, `zipalign`, `apksigner`). - override def androidSdkModule: AndroidSdkModule = sdkModule - - // Define the Android application name. Defaults to "HelloWorld", but can be customized - // by overriding this value in the project. - def androidAppName: T[String] = T { "HelloWorld" } } ////SNIPPET:END @@ -31,7 +24,7 @@ object `package` extends RootModule with AndroidAppModule { /** Usage -> ./mill androidApp +> ./mill androidApk */ @@ -42,11 +35,6 @@ object `package` extends RootModule with AndroidAppModule { // This Mill build configuration is designed to build a simple "Hello World" Android application. // By extending `AndroidAppModule`, we leverage its predefined Android build tasks, ensuring that // all necessary steps (resource generation, APK creation, and signing) are executed automatically. -// -// ### Customizable Fields: -// -// - **`androidAppName`**: This defines the name of the final APK file. By default, it's set to -// "HelloWorld.apk", but you can modify this value based on your project requirements. // // ### Project Structure: // The project follows the standard Android app layout. Below is a typical project folder structure: diff --git a/scalalib/src/mill/javalib/android/AndroidAppModule.scala b/scalalib/src/mill/javalib/android/AndroidAppModule.scala index eba55a65e9e..acbec541c98 100644 --- a/scalalib/src/mill/javalib/android/AndroidAppModule.scala +++ b/scalalib/src/mill/javalib/android/AndroidAppModule.scala @@ -52,17 +52,6 @@ trait AndroidAppModule extends JavaModule { */ def androidSdkModule: AndroidSdkModule - /** - * Defines the name of the Android application. - * - * The name is used as part of the APK file name (e.g., `HelloWorld.apk`), and also - * helps distinguish different builds. It defaults to "HelloWorld", but can be - * overridden for custom app names. - * - * @return The name of the Android application as a String. - */ - def androidAppName: T[String] = Task { "HelloWorld" } - /** * Generates the Android resources (such as layouts, strings, and other assets) needed for the application. * @@ -151,7 +140,7 @@ trait AndroidAppModule extends JavaModule { * @return A `PathRef` pointing to the generated JAR file. */ def androidJar: T[PathRef] = Task { - val jarFile: os.Path = T.dest / "my_classes.jar" + val jarFile: os.Path = T.dest / "app.jar" os.call( Seq( @@ -201,8 +190,8 @@ trait AndroidAppModule extends JavaModule { * * @return A `PathRef` pointing to the unsigned APK file. */ - def androidApk: T[PathRef] = Task { - val unsignedApk: os.Path = T.dest / s"${androidAppName().toString}.unsigned.apk" + def androidUnsignedApk: T[PathRef] = Task { + val unsignedApk: os.Path = T.dest / "app.unsigned.apk" os.call( Seq( @@ -233,8 +222,8 @@ trait AndroidAppModule extends JavaModule { * * @return A `PathRef` pointing to the aligned APK file. */ - def androidAlignApk: T[PathRef] = Task { - val alignedApk: os.Path = T.dest / s"${androidAppName().toString}.aligned.apk" + def androidAlignedUnsignedApk: T[PathRef] = Task { + val alignedApk: os.Path = T.dest / "app.aligned.apk" os.call( Seq( @@ -242,7 +231,7 @@ trait AndroidAppModule extends JavaModule { "-f", "-p", "4", // Force overwrite, align with 4-byte boundary - androidApk().path.toString, // Use the unsigned APK + androidUnsignedApk().path.toString, // Use the unsigned APK alignedApk.toString // Output aligned APK ) ) @@ -264,8 +253,8 @@ trait AndroidAppModule extends JavaModule { * * @return A `PathRef` pointing to the signed APK. */ - def androidApp: T[PathRef] = Task { - val signedApk: os.Path = millSourcePath / s"${androidAppName().toString}.apk" + def androidApk: T[PathRef] = Task { + val signedApk: os.Path = T.dest / "app.apk" os.call( Seq( @@ -281,7 +270,7 @@ trait AndroidAppModule extends JavaModule { "pass:android", // Key password "--out", signedApk.toString, // Output signed APK - androidAlignApk().path.toString // Use aligned APK + androidAlignedUnsignedApk().path.toString // Use aligned APK ) )