Gradle and Maven repositories
putnami observe reads a Java or Kotlin repository built with Gradle or
Maven, Android included, without a Putnami workspace. It never runs Gradle or
Maven: it reads the settings, the build files and the POMs as text, and the
reports the CI already wrote.
Gradle
A repository whose root holds settings.gradle.kts or settings.gradle is a
Gradle build. Each project the settings name in an include call with string
literals is a component of source workspace and ecosystem gradle, named by
its Gradle path:
rootProject.name = "shop"
include(":app", ":api", ":pricing")
include(":libs:core")
project(":legacy").projectDir = file("old/legacy"):app is the directory app and :libs:core is libs/core. A
projectDir set to file("dir") or File(settingsDir, "dir") moves the
project; File(rootDir, "dir") and File(rootProject.projectDir, "dir") read
the same way. The root project is a component only when the root holds a
src directory, as a single-project build does. An Android module, of
com.android.application or com.android.library, is a component like any
other: the detector does not read plugins.
The edges come from each project's build.gradle.kts or build.gradle,
outside comments:
| Reference | Names |
|---|---|
project(":pricing"), project(path: ":pricing"), project(path = ":pricing"), findProject(":pricing") |
:pricing |
project("impl") in :libs:core |
:libs:core:impl: a relative path is relative to the project |
projects.pricing |
:pricing, the type-safe project accessor |
projects.libs.sharedModel |
:libs:shared-model: kebab and snake case become camel case |
A reference to a project the settings do not include is no edge.
The read is static. An include argument or a projectDir value that is not
a string literal, such as a variable, a template or a loop over directories,
gets observe.gradle_dynamic_settings, and the projects the settings name with
literals stay components. So does an include without parentheses or quotes,
such as Groovy's include it, and a function reference, forEach(::include).
The word include inside a string or a comment is no include:
file("plugins").listFiles()?.forEach { include(":plugins:${it.name}") }Composite builds (includeBuild), buildSrc, convention plugins and the
dependencies a subprojects or allprojects block adds are not read.
Maven
A repository whose root holds pom.xml is a Maven build. The <modules> a
POM lists, or the <subprojects> of Maven 4, in the project and in its
profiles, are read recursively: each from the pom.xml of its directory, or
from the POM file the entry names. Each POM whose <packaging> is not pom
is a component of source workspace and ecosystem maven, named
groupId:artifactId, with the groupId of its <parent> when it states
none. An aggregator, of packaging pom, is no component.
A <dependency> of the project or of one of its profiles whose groupId and
artifactId name another module of the repository is an edge.
${project.groupId}, ${pom.groupId}, ${project.parent.groupId} and the
POM's own <properties> are resolved first. A dependency of
<dependencyManagement> or of a plugin is no edge. A parent POM outside the
repository and a BOM are not read.
Reports
Discovery finds the reports by their content. It walks every directory but
skips the build outputs that hold no report: build/intermediates,
build/generated, build/tmp and build/kotlin of Gradle and Android, and
target/classes, target/test-classes, target/generated-sources and
target/generated-test-sources of Maven.
| Tool | JUnit | Coverage |
|---|---|---|
| Gradle | build/test-results/<task>/TEST-*.xml |
build/reports/jacoco/<task>/*.xml, Kover's build/reports/kover/report.xml |
| Android | build/test-results/testDebugUnitTest/, build/outputs/androidTest-results/ |
build/reports/coverage/ |
| Maven | target/surefire-reports/TEST-*.xml, target/failsafe-reports/TEST-*.xml |
target/site/jacoco/jacoco.xml |
A JUnit case whose class name, without its $ nested class, names a .java
or .kt file under src/test/java, src/test/kotlin, src/androidTest/java
or src/androidTest/kotlin goes to the component of that file:
com.acme.pricing.PriceTest$Discounts is
src/test/kotlin/com/acme/pricing/PriceTest.kt. The report's own component is
tried first, then every component, so a report gathered at the root lands too.
A JaCoCo report names each file by its package path and source file name,
com/acme/pricing/Price.kt. The file maps to the first component whose
src/main/java or src/main/kotlin holds it, the report's own component
first. JaCoCo records which instructions of a line ran, not how often: a
covered line counts one hit. Kover writes the same format. Other source roots,
such as Kotlin Multiplatform's src/commonMain/kotlin, are not read.
Dependency changes
| File | Packages |
|---|---|
gradle.lockfile, settings-gradle.lockfile |
each group:artifact:version=… line, as group:artifact |
a version catalog, gradle/libs.versions.toml or another .versions.toml file of a gradle directory |
each library as group:artifact, each plugin as its id |
Both are dependency changes of ecosystem maven. A catalog entry takes the
version it states, the one its version.ref names in [versions], or the
strictly, require or prefer member of a rich version. An entry without a
version, such as a library a platform manages, is no package. A pkg:maven
package URL of an SBOM matches a change by group:artifact.
A Maven build has no lockfile, so it has no dependency change. An SBOM the CI
writes, with cyclonedx-maven-plugin, gives the licences and known
vulnerabilities of a lockfile change, not the change itself.
Diagnostics
| Code | Meaning |
|---|---|
observe.gradle_dynamic_settings |
The settings compute an include or a projectDir; the projects named with literals stay components. |
observe.gradle_project_missing |
An included project has no directory, or lies outside the repository. |
observe.gradle_settings_unreadable |
The settings file cannot be read. |
observe.maven_module_missing |
A listed module has no POM, or lies outside the repository. |
observe.maven_pom_invalid |
A POM does not parse or states no artifactId; its modules are not read. |
observe.maven_walk_bounded |
The detector stopped after 10,000 POMs. |