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}" + )) + } +}