Skip to content

Commit af9dad6

Browse files
authored
Merge pull request #3542 from scala-steward-org/topic/gradle-version-catalog
Extract dependencies from Gradle Version Catalogs
2 parents 91ff127 + 7ea401d commit af9dad6

File tree

13 files changed

+315
-11
lines changed

13 files changed

+315
-11
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ lazy val core = myCrossProject("core")
148148
Dependencies.monocleCore,
149149
Dependencies.refined,
150150
Dependencies.scalacacheCaffeine,
151+
Dependencies.tomlj,
151152
Dependencies.logbackClassic % Runtime,
152153
Dependencies.catsLaws % Test,
153154
Dependencies.circeLiteral % Test,

docs/repo-specific-configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ updates.allowPreReleases = [ { groupId = "com.example", artifactId="foo" } ]
130130
updates.limit = 5
131131

132132
# The extensions of files that should be updated.
133-
# Default: [".mill",".sbt",".sbt.shared",".sc",".scala",".scalafmt.conf",".sdkmanrc",".yml","build.properties","mill-version","pom.xml"]
133+
# Default: [".mill",".sbt",".sbt.shared",".sc",".scala",".scalafmt.conf",".sdkmanrc",".yml","build.properties","libs.versions.toml","mill-version","pom.xml"]
134134
updates.fileExtensions = [".scala", ".sbt", ".sbt.shared", ".sc", ".yml", ".md", ".markdown", ".txt"]
135135

136136
# If "on-conflicts", Scala Steward will update the PR it created to resolve conflicts as

modules/core/src/main/scala/org/scalasteward/core/application/Context.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.http4s.client.Client
2525
import org.http4s.headers.`User-Agent`
2626
import org.scalasteward.core.application.Config.ForgeCfg
2727
import org.scalasteward.core.buildtool.BuildToolDispatcher
28+
import org.scalasteward.core.buildtool.gradle.GradleAlg
2829
import org.scalasteward.core.buildtool.maven.MavenAlg
2930
import org.scalasteward.core.buildtool.mill.MillAlg
3031
import org.scalasteward.core.buildtool.sbt.SbtAlg
@@ -61,6 +62,7 @@ final class Context[F[_]](implicit
6162
val filterAlg: FilterAlg[F],
6263
val forgeRepoAlg: ForgeRepoAlg[F],
6364
val gitAlg: GitAlg[F],
65+
val gradleAlg: GradleAlg[F],
6466
val hookExecutor: HookExecutor[F],
6567
val httpJsonClient: HttpJsonClient[F],
6668
val logger: Logger[F],
@@ -176,6 +178,7 @@ object Context {
176178
implicit val versionsCache: VersionsCache[F] =
177179
new VersionsCache[F](config.cacheTtl, versionsStore)
178180
implicit val updateAlg: UpdateAlg[F] = new UpdateAlg[F]
181+
implicit val gradleAlg: GradleAlg[F] = new GradleAlg[F](config.defaultResolver)
179182
implicit val mavenAlg: MavenAlg[F] = new MavenAlg[F](config)
180183
implicit val sbtAlg: SbtAlg[F] = new SbtAlg[F](config)
181184
implicit val scalaCliAlg: ScalaCliAlg[F] = new ScalaCliAlg[F]

modules/core/src/main/scala/org/scalasteward/core/buildtool/BuildToolDispatcher.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.scalasteward.core.buildtool
1818

1919
import cats.Monad
2020
import cats.syntax.all.*
21+
import org.scalasteward.core.buildtool.gradle.GradleAlg
2122
import org.scalasteward.core.buildtool.maven.MavenAlg
2223
import org.scalasteward.core.buildtool.mill.MillAlg
2324
import org.scalasteward.core.buildtool.sbt.SbtAlg
@@ -29,6 +30,7 @@ import org.scalasteward.core.scalafmt.ScalafmtAlg
2930
import org.typelevel.log4cats.Logger
3031

3132
final class BuildToolDispatcher[F[_]](implicit
33+
gradleAlg: GradleAlg[F],
3234
logger: Logger[F],
3335
mavenAlg: MavenAlg[F],
3436
millAlg: MillAlg[F],
@@ -53,7 +55,7 @@ final class BuildToolDispatcher[F[_]](implicit
5355
buildTools.traverse_(_.runMigration(buildRoot, migration))
5456
})
5557

56-
private val allBuildTools = List(mavenAlg, millAlg, sbtAlg, scalaCliAlg)
58+
private val allBuildTools = List(gradleAlg, mavenAlg, millAlg, sbtAlg, scalaCliAlg)
5759
private val fallbackBuildTool = List(sbtAlg)
5860

5961
private def findBuildTools(buildRoot: BuildRoot): F[(BuildRoot, List[BuildToolAlg[F]])] =
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2018-2025 Scala Steward contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.scalasteward.core.buildtool.gradle
18+
19+
import better.files.File
20+
import cats.Monad
21+
import cats.syntax.all.*
22+
import org.scalasteward.core.buildtool.{BuildRoot, BuildToolAlg}
23+
import org.scalasteward.core.data.Scope.Dependencies
24+
import org.scalasteward.core.data.{Resolver, Scope}
25+
import org.scalasteward.core.io.{FileAlg, WorkspaceAlg}
26+
import org.typelevel.log4cats.Logger
27+
28+
final class GradleAlg[F[_]](defaultResolver: Resolver)(implicit
29+
fileAlg: FileAlg[F],
30+
override protected val logger: Logger[F],
31+
workspaceAlg: WorkspaceAlg[F],
32+
F: Monad[F]
33+
) extends BuildToolAlg[F] {
34+
override def name: String = "Gradle"
35+
36+
override def containsBuild(buildRoot: BuildRoot): F[Boolean] =
37+
libsVersionsToml(buildRoot).flatMap(fileAlg.isRegularFile)
38+
39+
override def getDependencies(buildRoot: BuildRoot): F[List[Dependencies]] =
40+
libsVersionsToml(buildRoot)
41+
.flatMap(fileAlg.readFile)
42+
.map(_.getOrElse(""))
43+
.map(gradleParser.parseDependenciesAndPlugins)
44+
.map { case (dependencies, plugins) =>
45+
val ds = Option.when(dependencies.nonEmpty)(Scope(dependencies, List(defaultResolver)))
46+
val ps = Option.when(plugins.nonEmpty)(Scope(plugins, List(pluginsResolver)))
47+
ds.toList ++ ps.toList
48+
}
49+
50+
private def libsVersionsToml(buildRoot: BuildRoot): F[File] =
51+
workspaceAlg.buildRootDir(buildRoot).map(_ / "gradle" / libsVersionsTomlName)
52+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2018-2025 Scala Steward contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.scalasteward.core.buildtool.gradle
18+
19+
import cats.implicits.*
20+
import org.scalasteward.core.data.{ArtifactId, Dependency, GroupId, Version}
21+
import org.tomlj.{Toml, TomlTable}
22+
import scala.jdk.CollectionConverters.*
23+
24+
object gradleParser {
25+
def parseDependenciesAndPlugins(input: String): (List[Dependency], List[Dependency]) = {
26+
val parsed = Toml.parse(input)
27+
val versionsTable = getTableSafe(parsed, "versions")
28+
val librariesTable = getTableSafe(parsed, "libraries")
29+
val pluginsTable = getTableSafe(parsed, "plugins")
30+
31+
val dependencies = collectEntries(librariesTable, parseDependency(_, versionsTable))
32+
val plugins = collectEntries(pluginsTable, parsePlugin(_, versionsTable))
33+
34+
(dependencies, plugins)
35+
}
36+
37+
private def collectEntries[A: Ordering](table: TomlTable, f: TomlTable => Option[A]): List[A] = {
38+
val aSet = table.entrySet().asScala.map(_.getValue).flatMap {
39+
case t: TomlTable => f(t)
40+
case _ => None
41+
}
42+
aSet.toList.sorted
43+
}
44+
45+
private def parseDependency(lib: TomlTable, versions: TomlTable): Option[Dependency] =
46+
for {
47+
case (groupId, artifactId) <- parseModuleObj(lib).orElse(parseModuleString(lib))
48+
version <- parseVersion(lib, versions)
49+
} yield Dependency(groupId, artifactId, version)
50+
51+
private def parseModuleObj(lib: TomlTable): Option[(GroupId, ArtifactId)] =
52+
for {
53+
groupId <- getStringSafe(lib, "group").map(GroupId(_))
54+
artifactId <- getStringSafe(lib, "name").map(ArtifactId(_))
55+
} yield (groupId, artifactId)
56+
57+
private def parseModuleString(lib: TomlTable): Option[(GroupId, ArtifactId)] =
58+
getStringSafe(lib, "module").flatMap {
59+
_.split(':') match {
60+
case Array(g, a) => Some((GroupId(g), ArtifactId(a)))
61+
case _ => None
62+
}
63+
}
64+
65+
private def parsePlugin(plugin: TomlTable, versions: TomlTable): Option[Dependency] =
66+
for {
67+
id <- getStringSafe(plugin, "id")
68+
groupId = GroupId(id)
69+
artifactId = ArtifactId(s"$id.gradle.plugin")
70+
version <- parseVersion(plugin, versions)
71+
} yield Dependency(groupId, artifactId, version)
72+
73+
private def parseVersion(table: TomlTable, versions: TomlTable): Option[Version] = {
74+
def versionString = getStringSafe(table, "version")
75+
def versionRef = getStringSafe(table, "version.ref").flatMap(getStringSafe(versions, _))
76+
versionString.orElse(versionRef).map(Version.apply)
77+
}
78+
79+
private def getTableSafe(table: TomlTable, key: String): TomlTable =
80+
Option
81+
.when(table.contains(key) && table.isTable(key))(table.getTableOrEmpty(key))
82+
.getOrElse(emptyTable)
83+
84+
private val emptyTable: TomlTable = Toml.parse("")
85+
86+
private def getStringSafe(table: TomlTable, key: String): Option[String] =
87+
Option.when(table.contains(key) && table.isString(key))(table.getString(key))
88+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2018-2025 Scala Steward contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.scalasteward.core.buildtool
18+
19+
import org.scalasteward.core.data.Resolver
20+
21+
package object gradle {
22+
val libsVersionsTomlName = "libs.versions.toml"
23+
24+
val pluginsResolver: Resolver.MavenRepository =
25+
Resolver.MavenRepository("gradle-plugins", "https://plugins.gradle.org/m2/", None, None)
26+
}

modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ import eu.timepit.refined.types.numeric.NonNegInt
2222
import io.circe.generic.semiauto.deriveCodec
2323
import io.circe.refined.*
2424
import io.circe.{Codec, Decoder}
25-
import org.scalasteward.core.buildtool.maven.pomXmlName
26-
import org.scalasteward.core.buildtool.mill.MillAlg
27-
import org.scalasteward.core.buildtool.sbt.buildPropertiesName
25+
import org.scalasteward.core.buildtool.{gradle, maven, mill, sbt}
2826
import org.scalasteward.core.data.{GroupId, Update}
29-
import org.scalasteward.core.scalafmt.scalafmtConfName
27+
import org.scalasteward.core.scalafmt
3028
import org.scalasteward.core.update.FilterAlg.{
3129
FilterResult,
3230
IgnoredByConfig,
@@ -106,16 +104,17 @@ object UpdatesConfig {
106104
val defaultFileExtensions: Set[String] =
107105
Set(
108106
".mill",
109-
MillAlg.millVersionName,
110107
".sbt",
111108
".sbt.shared",
112109
".sc",
113110
".scala",
114-
scalafmtConfName,
115111
".sdkmanrc",
116112
".yml",
117-
buildPropertiesName,
118-
pomXmlName
113+
gradle.libsVersionsTomlName,
114+
maven.pomXmlName,
115+
mill.MillAlg.millVersionName,
116+
sbt.buildPropertiesName,
117+
scalafmt.scalafmtConfName
119118
)
120119

121120
val defaultLimit: Option[NonNegInt] = None

modules/core/src/test/scala/org/scalasteward/core/buildtool/BuildToolDispatcherTest.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ class BuildToolDispatcherTest extends FunSuite {
4141
}
4242

4343
val expectedState = initial.copy(trace =
44-
Cmd("test", "-f", s"$repoDir/pom.xml") +:
44+
Cmd("test", "-f", s"$repoDir/gradle/libs.versions.toml") +:
45+
Cmd("test", "-f", s"$repoDir/pom.xml") +:
4546
Cmd("test", "-f", s"$repoDir/build.sc") +:
4647
Cmd("test", "-f", s"$repoDir/build.mill") +:
4748
Cmd("test", "-f", s"$repoDir/build.mill.scala") +:
4849
Cmd("test", "-f", s"$repoDir/build.sbt") +:
4950
allGreps ++:
51+
Cmd("test", "-f", s"$repoDir/mvn-build/gradle/libs.versions.toml") +:
5052
Cmd("test", "-f", s"$repoDir/mvn-build/pom.xml") +:
5153
Cmd("test", "-f", s"$repoDir/mvn-build/build.sc") +:
5254
Cmd("test", "-f", s"$repoDir/mvn-build/build.mill") +:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.scalasteward.core.buildtool.gradle
2+
3+
import munit.CatsEffectSuite
4+
import org.scalasteward.core.TestSyntax.*
5+
import org.scalasteward.core.buildtool.BuildRoot
6+
import org.scalasteward.core.data.{Repo, Scope}
7+
import org.scalasteward.core.mock.MockContext.context.*
8+
import org.scalasteward.core.mock.{MockEffOps, MockState}
9+
10+
class GradleAlgTest extends CatsEffectSuite {
11+
test("getDependencies") {
12+
val repo = Repo("gradle-alg", "test-getDependencies")
13+
val buildRoot = BuildRoot(repo, ".")
14+
val buildRootDir = workspaceAlg.buildRootDir(buildRoot).unsafeRunSync()
15+
16+
val initial = MockState.empty.addFiles(
17+
buildRootDir / "gradle" / libsVersionsTomlName ->
18+
"""|[libraries]
19+
|tomlj = { group = "org.tomlj", name = "tomlj", version = "1.1.1" }
20+
|[plugins]
21+
|kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.1.20-Beta1" }
22+
|""".stripMargin
23+
)
24+
val obtained = initial.flatMap(gradleAlg.getDependencies(buildRoot).runA)
25+
val kotlinJvm =
26+
"org.jetbrains.kotlin.jvm".g % "org.jetbrains.kotlin.jvm.gradle.plugin".a % "2.1.20-Beta1"
27+
val expected = List(
28+
List("org.tomlj".g % "tomlj".a % "1.1.1").withMavenCentral,
29+
Scope(List(kotlinJvm), List(pluginsResolver))
30+
)
31+
assertIO(obtained, expected)
32+
}
33+
}

0 commit comments

Comments
 (0)