Magic Modules

An experimental Gradle Plugin that automatically maps and includes modules in your builds.

magicmodules-demo

What is this?

// Blog post with the full motivation to come! Stay tunned

For large Android projects hosted in mono repos, management for module names might be a real pain, specially when we have lots of moving parts under a structure driven by nested Gradle subprojects.

This experimental plugin attemps to solve that. It parses a project tree like this

.
├── app
│   └── src
│       └── main
│           ├── AndroidManifest.xml
│           ├── java
│           └── res
├── build.gradle
├── buildSrc
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── kotlin
├── common
│   ├── core
│   │   ├── build.gradle
│   │   └── src
│   │       └── main
│   └── utils
│       ├── build.gradle.kts
│       └── src
│           └── main
├── features
│   ├── home
│   │   ├── build.gradle
│   │   └── src
│   │       └── main
│   └── login
│       ├── build.gradle
│       └── src
│           └── main
|
|
└── settings.gradle

and

  • it automatically includes all founded modules in settings.gradle
  • it writes 2 Kotlin files under your buildSrc/src/main/kotlin :
// Generated by MagicModules plugin. Mind your Linters!
import kotlin.String
import kotlin.collections.List

object Libraries {
    const val FEATURES_HOME: String = ":features:home"

    const val FEATURES_LOGIN: String = ":features:login"

    const val COMMON_CORE: String = ":common:core"

    const val COMMON_UTILS: String = ":common:utils"

    val allAvailable: List<String> = 
            listOf(
                FEATURES_HOME,
                FEATURES_LOGIN,
                COMMON_CORE,
                COMMON_UTILS
            )
}
// Generated by MagicModules plugin. Mind your Linters!
import kotlin.String
import kotlin.collections.List

object Applications {
    const val APP: String = ":app"

    val allAvailable: List<String> = 
            listOf(
                APP
            )
}

In this way, refactors around the project structure will become a bit easier, since build.gradle configuration

dependencies {
    implementation project(Libraries.COMMON_UTILS)
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    implementation ...
}

will break if common/utils moves around. The new constant under buildSrc will be mapped and will be ready to use.

We can also add all the libraries to one monotlithic app easily

dependencies {
    Libraries.allAvailable.each { implementation project(it) }
}

Setup

To try this plugin out, you can grab a snapshot build from Jitpack. Add this snippet in your settings.gradle file

buildscript {
    repositories {
        mavenCentral()	
        maven { url 'https://jitpack.io' }
    }

    dependencies {
        classpath 'com.github.dotanuki-labs:magic-modules:<plugin-version>'
    }
}

apply plugin: "io.labs.dotanuki.magicmodules"

and remove all include statements

include 'app'
include 'featureA'
include 'featureB'
include 'featureC'
include ...

They are not needed anymore.

If your project uses a multi-application layout, with standalone apps for your features/screens, you can opt-in to not include all com.android.application modules in order to reduce configuration times locally and eventually build times on CI.

rootProject.name='awesome-project'

apply plugin: "io.labs.dotanuki.magicmodules"

magicModules {
    includeApps = false
}

include ':app'

Matching Gradle build files

This plugin walks your project tree and inspect all the build.gradle and build.gradle.kts files in order to learn if the related module matches an Android library, a JVM library or an Android application. This means that Magic Modules is sensitive on how you apply plugins in your Gradle build scripts, for instance using

apply plugin: 'com.android.library'

or

plugins {
    kotlin("jvm")
}

This plugin does a best-effort attempt in order to catch all the common cases, but it might not work at all if you

  • (1) have some strategy to share build logic accross Gradle modules and
  • (2) applied the application or library plugin using such shared build logic for your modules

Building and testing

To build this plugin and publish it locally for testing

./gradlew publishToMavenLocal

To run all the checks, including integration tests

./gradlew ktlintCheck test

To check logs generated by this plugin and learn how this plugin works, we have a sample project available

cd sample
./gradlew clean app:assembleDebug --info | grep MagicModulesPlugin

Limitations

The main limitation I've found with this approach is that - right now - the plugin generates the Libraries.kt and Applications.kt under the main source set of buildSrc, which means eventually issues with linters that run for buildSrc files.

I need more time in order to figure out if we can have such generated files under buildSrc/build somehow.

Further work

I realised that

  • It might be useful to configure the output folder/package for Libraries.kt and Applications.kt
  • It might be useful to grab more Gradle build script matchers using the plugin configuration

Author

Coded by Ubiratan Soares (follow me on Twitter)

GitHub