All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mill.javalib.android.AndroidSdkModule.scala Maven / Gradle / Ivy

The newest version!
package mill.javalib.android

import coursier.MavenRepository
import mill._

import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import scala.xml.XML

/**
 * Trait for managing the Android SDK in a Mill build system.
 *
 * 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.
 *
 * For more information, refer to the official Android
 * [[https://developer.android.com/studio documentation]].
 */
@mill.api.experimental
trait AndroidSdkModule extends Module {

  // this has a format `repository2-%d`, where the last number is schema version. For the needs of this module it
  // is okay to stick with the particular version.
  private val remotePackagesUrl = "https://dl.google.com/android/repository/repository2-3.xml"

  /**
   * Specifies the version of the Android Bundle tool to be used.
   */
  def bundleToolVersion: T[String] = "1.17.2"

  /**
   * Specifies the version of the Manifest Merger.
   */
  def manifestMergerVersion: T[String] = "31.7.3"

  /**
   * Specifies the version of the Android build tools to be used.
   */
  def buildToolsVersion: T[String]

  /**
   * Specifies the Android platform version (e.g., Android API level).
   */
  def platformsVersion: T[String] = Task { "android-" + buildToolsVersion().split('.').head }

  /**
   * URL to download bundle tool, used for creating Android app bundles (AAB files).
   */
  def bundleToolUrl: T[String] = Task {
    s"https://github.com/google/bundletool/releases/download/${bundleToolVersion()}/bundletool-all-${bundleToolVersion()}.jar"
  }

  /**
   * Provides the path to the `bundleTool.jar` file, necessary for creating Android bundles.
   *
   * For More Read Bundle Tool [[https://developer.android.com/tools/bundletool Documentation]]
   */
  def bundleToolPath: T[PathRef] = Task {
    val bundleToolJar = Task.dest / "bundleTool.jar"
    os.write(bundleToolJar, requests.get(bundleToolUrl()).bytes)
    PathRef(bundleToolJar)
  }

  /**
   * Provides the path to the `android.jar` file, necessary for compiling Android apps.
   */
  def androidJarPath: T[PathRef] = Task {
    installAndroidSdkComponents()
    PathRef(sdkPath().path / "platforms" / platformsVersion() / "android.jar")
  }

  /**
   * Provides path to the Android build tools for the selected version.
   */
  def buildToolsPath: T[PathRef] = Task {
    installAndroidSdkComponents()
    PathRef(sdkPath().path / "build-tools" / buildToolsVersion())
  }

  /**
   * Provides path to the Android CLI lint tool.
   */
  def lintToolPath: T[PathRef] = Task {
    installAndroidSdkComponents()
    PathRef(sdkPath().path / "cmdline-tools/latest/bin/lint")
  }

  /**
   * Provides path to D8 Dex compiler, used for converting Java bytecode into Dalvik bytecode.
   *
   * For More Read D8 [[https://developer.android.com/tools/d8 Documentation]]
   */
  def d8Path: T[PathRef] = Task {
    PathRef(buildToolsPath().path / "d8")
  }

  /**
   * Provides the path to AAPT2, used for resource handling and APK packaging.
   *
   * For More Read AAPT2 [[https://developer.android.com/tools/aapt2 Documentation]]
   */
  def aapt2Path: T[PathRef] = Task {
    PathRef(buildToolsPath().path / "aapt2")
  }

  /**
   * Provides the path to the Zipalign tool, which optimizes APK files by aligning their data.
   *
   * For More Read Zipalign [[https://developer.android.com/tools/zipalign Documentation]]
   */
  def zipalignPath: T[PathRef] = Task {
    PathRef(buildToolsPath().path / "zipalign")
  }

  def fontsPath: T[PathRef] = Task {
    PathRef(sdkPath().path / "fonts")
  }

  /**
   * Provides the path to the APK signer tool, used to digitally sign APKs.
   *
   * For More Read APK Signer [[https://developer.android.com/tools/apksigner Documentation]]
   */
  def apksignerPath: T[PathRef] = Task {
    PathRef(buildToolsPath().path / "apksigner")
  }

  /**
   * Provides the path for the Android Debug Bridge (adt) tool.
   *
   * For more information, refer to the official Android documentation [[https://developer.android.com/tools/adb]]
   */
  def adbPath: T[PathRef] = Task {
    PathRef(sdkPath().path / "platform-tools/adb")
  }

  /**
   * Provides the path for the Android Virtual Device Manager (avdmanager) tool
   *
   *  For more information refer to the official Android documentation [[https://developer.android.com/tools/avdmanager]]
   */
  def avdPath: T[PathRef] = Task {
    PathRef(sdkPath().path / "cmdline-tools/latest/bin/avdmanager")
  }

  /**
   * Provides the path for the android emulator tool
   *
   * For more information refer to [[https://developer.android.com/studio/run/emulator]]
   */
  def emulatorPath: T[PathRef] = Task {
    PathRef(sdkPath().path / "emulator/emulator")
  }

  /**
   * Provides the path for the Android SDK Manager tool
   *
   * @return A task containing a [[PathRef]] pointing to the SDK directory.
   */
  def sdkManagerPath: T[PathRef] = Task {
    PathRef(sdkPath().path / "cmdline-tools/latest/bin/sdkmanager")
  }

  /**
   * Installs the necessary Android SDK components such as platform-tools, build-tools, and Android platforms.
   *
   * For more details on the `sdkmanager` tool, refer to:
   * [[https://developer.android.com/tools/sdkmanager sdkmanager Documentation]]
   */
  def installAndroidSdkComponents: T[Unit] = Task {
    val sdkPath0 = sdkPath()
    val sdkManagerPath = findLatestSdkManager(sdkPath0.path) match {
      case Some(x) => x
      case _ => throw new IllegalStateException(
          s"Cannot locate cmdline-tools in Android SDK $sdkPath0. Download" +
            " it at https://developer.android.com/studio#command-tools. See https://developer.android.com/tools" +
            " for more details."
        )
    }

    val packages = Seq(
      "platform-tools",
      s"build-tools;${buildToolsVersion()}",
      s"platforms;${platformsVersion()}",
      "cmdline-tools;latest"
    )
    // sdkmanager executable and state of the installed package is a shared resource, which can be accessed
    // from the different Android SDK modules.
    AndroidSdkLock.synchronized {
      val missingPackages = packages.filter(p => !isPackageInstalled(sdkPath0.path, p))
      val packagesWithoutLicense = missingPackages
        .map(p => (p, isLicenseAccepted(sdkPath0.path, remoteReposInfo().path, p)))
        .filter(!_._2)
      if (packagesWithoutLicense.nonEmpty) {
        throw new IllegalStateException(
          "Failed to install the following SDK packages, because their respective" +
            s" licenses are not accepted:\n\n${packagesWithoutLicense.map(_._1).mkString("\n")}"
        )
      }

      if (missingPackages.nonEmpty) {
        val callResult = os.call(
          // Install platform-tools, build-tools, and the Android platform
          Seq(sdkManagerPath.toString) ++ missingPackages,
          stdout = os.Inherit
        )
        if (callResult.exitCode != 0) {
          throw new IllegalStateException(
            "Failed to install Android SDK components. Check logs for more details."
          )
        }
      }
    }
  }

  private def sdkPath: T[PathRef] = Task {
    Task.env.get("ANDROID_HOME")
      .orElse(Task.env.get("ANDROID_SDK_ROOT")) match {
      case Some(x) => PathRef(os.Path(x))
      case _ => throw new IllegalStateException("Android SDK location not found. Define a valid" +
          " SDK location with an ANDROID_HOME environment variable.")
    }
  }

  private def isPackageInstalled(sdkPath: os.Path, packageName: String): Boolean =
    os.exists(sdkPath / os.SubPath(packageName.replaceAll(";", "/")))

  private def isLicenseAccepted(
      sdkPath: os.Path,
      remoteReposInfo: os.Path,
      packageName: String
  ): Boolean = {
    val (licenseName, licenseHash) = licenseForPackage(remoteReposInfo, packageName)
    val licenseFile = sdkPath / "licenses" / licenseName
    os.exists(licenseFile) && os.isFile(licenseFile) && os.read(licenseFile).contains(licenseHash)
  }

  private def licenseForPackage(remoteReposInfo: os.Path, packageName: String): (String, String) = {
    val repositoryInfo = XML.loadFile(remoteReposInfo.toIO)
    val remotePackage = (repositoryInfo \ "remotePackage")
      .filter(_ \@ "path" == packageName)
      .head
    val licenseName = (remotePackage \ "uses-license").head \@ "ref"
    val licenseText = (repositoryInfo \ "license")
      .filter(_ \@ "id" == licenseName)
      .text
      .replaceAll(
        "(?<=\\s)[ \t]*",
        ""
      ) // remove spaces and tabs preceded by space, tab, or newline.
      .replaceAll("(?
      val candidates = os.list(sdkPath / "cmdline-tools")
        .filter(os.isDir)
      if (candidates.nonEmpty) {
        val latestCmdlineToolsPath = candidates
          .map(p => (p, p.baseName.split('.')))
          .filter(_._2 match {
            case Array(_, _) => true
            case _ => false
          })
          .maxBy(_._2.head.toInt)._1
        sdkManagerPath = latestCmdlineToolsPath / "bin/sdkmanager"
      }
    }
    Some(sdkManagerPath).filter(os.exists)
  }

}

private object AndroidSdkLock

object AndroidSdkModule {

  /**
   * Declaration of the Maven Google Repository.
   */
  val mavenGoogle: MavenRepository = MavenRepository("https://maven.google.com/")
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy