A Gradle plugin that helps keep your module graph healthy and lean.
- Medium Article with complete context.
- Talk about module graph and why it matters
- Changelog
- Build speeds can be very dependent on the structure of your module graph.
- Modules separate logical units and enforce proper dependencies.
- The module graph can silently degenerate into a list-like structure.
- Breaking problematic module dependencies can be very difficult, it is cheaper to prevent them.
- If not enforced, undesirable module dependencies will appear. Murphy's law of dependencies: "Whatever they can access, they will access."
- The plugin provides a simple way for defining rules, which will be verified with the task
assertModuleGraph
as part of thecheck
task. - Match module names using regular expressions.
- Define the only allowed dependencies between modules
allowed = [':feature-one -> :feature-[a-z-:]*', ':.* -> :core', ':feature.* -> :lib.*']
define rules by usingregex -> regex
signature.- Dependency, which will not match any of those rules will fail the assertion.
restricted [':feature-[a-z]* -X> :forbidden-to-depend-on']
helps us to define custom rules by usingregex -X> regex
signature.maxHeight = 4
can verify that the height of the modules tree with a root in the module will not exceed 4. Tree height is a good metric to prevent module tree degeneration into a list.
Apply the plugin to a module, which dependencies graph you want to assert.
plugins {
id "com.jraska.module.graph.assertion" version "2.7.1"
}
- You can run
./gradlew assertModuleGraph
to execute configured checks or./gradlew check
whereassertModuleGraph
will be included. - Alternative option is using
assertOnAnyBuild = true
configuration to run the checks on every single Gradle build without need for running explicit tasks - see #184 for more details. - Hint: Gradle Configuration On Demand may hide some modules from the plugin visibility. If you notice some modules are missing, try the
--no-configure-on-demand
flag.
Rules are applied on the Gradle module and its api
and implementation
dependencies by default. Typically you would want to apply this in your final app module, however configuration for any module is possible. Example
moduleGraphAssert {
maxHeight = 4
allowed = [':.* -> :core', ':feature.* -> :lib.*'] // regex to match module names
restricted = [':feature-[a-z]* -X> :forbidden-to-depend-on'] // regex to match module names
configurations = ['api', 'implementation'] // Dependency configurations to look. ['api', 'implementation'] is the default
assertOnAnyBuild = false // true value will run the assertions as part of any build without need to run the assert* tasks, false is default
}
configurations += setOf("commonMainImplementation", "commonMainApi") // different sourceSets defaults
- Please see this issue for details and comment if you face issues.
- You don't have to rely on module names and set a property
ext.moduleNameAssertAlias = "ThisWillBeAssertedOn"
- This can be set on any module and the
allowed
/restricted
rules would use the alias instead of module name for asserting. - This is useful for example if you want to use "module types" where each module has a type regardless the name and you want to manage only dependnecies of different types.
- It is recommended to use either module names or
moduleNameAssertAlias
everywhere. Mixing both is not recommended. - Example of module rules you could implement for a flat module graph:
- Each module would have set
ext.moduleNameAssertAlias = "Api|Implementation|App"
- Module rules example for such case:
allowed = ['Implementation -> Api', 'App -> Implementation', 'App -> Api']
- In case you want to migrate to this structure incrementally, you can set a separate module type like
ext.moduleNameAssertAlias = "NeedsMigration"
and setting
allowed = [
'Implementation -> Api',
'App -> Implementation',
'App -> Api',
'NeedsMigration -> .*',
'.* -> NeedsMigration'
]
"NeedsMigration"
modules can be then tackled one by one to move them intoImplementation
orApi
type. Example of app with this structure can be seen here.
- Visualising the graph could be useful to help find your dependency issues, therefore a helper
generateModulesGraphvizText
task is included. - This generates a graph of dependent modules when the plugin is applied.
- The longest path of the project is in red.
- If you utilise Configuration on demand Gradle feature, please use
--no-configure-on-demand
flag along thegenerateModulesGraphvizText
task. - You can set the
modules.graph.of.module
parameter if you are only interested in a sub-graph of the module graph.
./gradlew generateModulesGraphvizText -Pmodules.graph.of.module=:feature-one
- Adding the parameter
modules.graph.output.gv
saves the graphViz file to the specified path
./gradlew generateModulesGraphvizText -Pmodules.graph.output.gv=all_modules
- Executing the task
generateModulesGraphStatistics
prints the information about the graph. - Statistics printed: Modules Count, Edges Count, Height and Longest Path
- Parameter
-Pmodules.graph.of.module
is supported as well.
./gradlew generateModulesGraphStatistics -Pmodules.graph.of.module=:feature-one
Please feel free to create PR or issue with any suggestions or ideas. No special format required, just common sense.
Setting up a composite build
Composite builds are consumed directly without publishing a version.
settings.gradle:
includeBuild("path/to/modules-graph-assert")
Root build.gradle:
plugins {
id('com.jraska.module.graph.assertion')
}