Skip to content

Ra/build/convention plugins - #7

Merged
ramyassaf merged 7 commits into
mainfrom
ra/build/convention-plugins
Jun 28, 2026
Merged

Ra/build/convention plugins#7
ramyassaf merged 7 commits into
mainfrom
ra/build/convention-plugins

Conversation

@ramyassaf

Copy link
Copy Markdown
Owner

No description provided.

Ramy ASSAF added 7 commits June 12, 2026 10:25
…atalog

Expose the version catalog to the build-logic included build so upcoming
convention plugins read versions and coordinates from the same single
source of truth as every module:

- Declare the Android SDK levels (compileSdk, minSdk, targetSdk) and the
  JVM target in [versions]. Today each Android module repeats these as
  literals; centralizing them first means the next SDK bump becomes a
  one-line change.
- Add the AGP and Kotlin Gradle plugin artifacts to [libraries] and wire
  them as compileOnly dependencies of :convention. Convention plugins
  compile against those APIs while the main build keeps providing them at
  runtime via the root plugins block, so a single copy stays on the build
  classpath.
- Import the catalog in build-logic/settings.gradle.kts; an included
  build does not inherit the main build catalog automatically.
- Merge the two identical kotlinxCoroutinesCore/kotlinxCoroutinesTest
  version refs into one kotlinxCoroutines ref in catalog.

No module configuration changes yet.
…on plugins

Every module now declares its build archetype through one convention
plugin instead of repeating toolchain configuration:

- com.compose.chi.android.application: AGP application plugin, shared
  Android conventions, target SDK, and packaging rules (:app).
- com.compose.chi.android.library: AGP library plugin plus the same
  shared Android conventions (:data, :presentation).
- com.compose.chi.kotlin.jvm: Kotlin JVM plugin pinned to the project
  JVM toolchain (:domain).

Both Android plugins funnel through a single configureKotlinAndroid
helper that reads compileSdk, minSdk, and the Java level from the
version catalog, so the application and library archetypes cannot drift
apart and the next SDK bump is a one-line catalog change instead of a
three-file edit.

Module build scripts keep only what genuinely distinguishes them:
namespace, application identity and build types, opt-in features such
as test fixtures, and their dependencies. Compose wiring intentionally
stays inline in this step; it becomes its own composable convention
plugin next.
…n plugin

Compose is a capability layered on top of a module archetype, not an
archetype of its own, so com.compose.chi.android.compose stacks on
either Android convention plugin instead of existing once per archetype.
It now owns the whole Compose toolchain in one place:

- the Compose compiler Gradle plugin, keeping the Kotlin and Compose
  compiler versions in lockstep,
- the compose build feature flag,
- the Compose BOM on the implementation classpath, so modules declare
  Compose artifacts without versions,
- preview tooling: ui-tooling-preview to compile @Preview composables,
  with the full ui-tooling inspector on debug builds only.

The plugin fails fast with an actionable message when applied to a
non-Android module. Which Compose libraries a module actually uses
(material3, navigation, ...) stays declared in the module itself:
conventions own toolchain concerns, modules own their feature surface.
Two dependency groups were declared once per module without ever varying,
which makes them platform policy rather than module choice. Move them
where policy lives:

- com.compose.chi.koin: a new convention plugin owning the Koin BOM and
  koin-android for the three DI-wired modules (:app, :presentation,
  :data). The BOM on the implementation classpath also versions any
  extra Koin artifact a module adds itself, so :presentation keeps only
  its module-specific, version-free koin-androidx-compose line.
  :domain never applies it - the domain layer stays DI-framework free
  by architecture policy.
- Universal test policy: JUnit 4 and Konsist move into all three base
  conventions. Every module colocates Konsist architecture tests with
  the code they verify, so a new module is born with that toolchain on
  its test classpath and the policy cannot be forgotten.

The line drawn on purpose: test libraries that describe what a module
actually exercises (MockK, Turbine, coroutines-test) and instrumented
test dependencies stay declared per module. A build script should still
answer the question of what its module really uses for everything that
is a genuine choice.
The verifyModuleArchitecture task previously walked the project model
inside a doLast action. Reading Project state at execution time is
exactly what the configuration cache forbids, so the check was the one
thing standing between this build and enabling the cache.

Split the work along the configuration/execution boundary:

- ModuleArchitecturePlugin now snapshots the actual module graph through
  lazy providers. Gradle evaluates them when fingerprinting task inputs,
  after every module has been configured, so the snapshot still sees the
  final declared dependencies of each module.
- A new VerifyModuleArchitectureTask receives the contract and the
  snapshot as plain @input value properties and compares them in its
  action without ever touching Project state.

Same contract, same violation messages, two improvements for free: the
value inputs participate in up-to-date checking, and a contract module
missing from the build now reports as a normal violation instead of an
unknown-project failure.
…essors

With every task in the build configuration-cache compatible, turn on the
Gradle execution features a modular build is shaped for:

- org.gradle.configuration-cache=true: skip the configuration phase
  entirely when build scripts have not changed; warm verification runs
  drop to about a second.
- org.gradle.parallel=true: the enforced module graph keeps projects
  decoupled, which is exactly what makes parallel execution safe.
- org.gradle.caching=true: reuse task outputs across builds and
  branches from the local build cache.
- Daemon heap raised to 4 GB; 2 GB is tight for AGP 9 plus Kotlin 2.3
  across four modules.

Module dependencies also move from string paths to type-safe project
accessors (TYPESAFE_PROJECT_ACCESSORS): implementation(projects.domain)
is compile-checked, so a renamed or mistyped module path now fails the
build instead of being discovered at resolution time. The architecture
verification keeps seeing accessor-declared dependencies, since they
are ordinary ProjectDependency instances.
Update the documentation to match the build after the convention-plugin
extraction:

- docs/modularization.md: the Gradle plumbing walkthrough now shows the
  real build scripts - archetype convention plugins instead of repeated
  android blocks - plus a table of the six plugin ids and what each
  owns, the settings with build-logic and type-safe accessors, and the
  policy line for which dependencies live in conventions versus
  modules. The test-fixtures consumer snippet now lists the actual
  consumers (:data and :presentation).
- README.md: build row of the tech stack, project tree with
  build-logic, technologies checklist, and a roadmap entry for the
  build conventions and Gradle modernization work.
- CHANGELOG.md: new section covering the convention plugins, catalog
  centralization, configuration-cache compatible verification, and the
  enabled Gradle execution features, with the verification steps.
@ramyassaf
ramyassaf merged commit 6affc05 into main Jun 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant