Skip to content

Commit b17d041

Browse files
authored
Implement GMavenService (#6644)
Per [b/392134866](https://b.corp.google.com/issues/392134866), This implements a centralized interface for communicating with GMaven called `GMavenService`. This service implements the gradle build-service interface to provide proper parallel access, and keeps local `ConcurrentHashMap` instances to cache responses. Cached responses are on a _per-build_ basis, to avoid improper caching of dynamic data. That is, given any build- **all** tasks that utilize `GMavenService` will share the responses from GMaven; even within a parallel environment. But if the tasks are considered out-of-date and are ran again, then new requests will be made to the GMaven backend. Tests and documentation are provided for everything added as well. Note that while this PR _implements_ `GMavenService`- it does _not_ refactor the existing `GMavenHelper` and `RepositoryClient` usages to use it. That will occur in subsequent PRs, as to avoid polluting this PR. Furthermore, while there are no tests for `PomElement` directly- in a future PR that includes tests for bom generation, `PomElement` will be tested as a by-product. This PR also fixes the following: - [b/392135224](https://b.corp.google.com/issues/392135224) -> Implement centralized pom datamodel
1 parent 3f23c5f commit b17d041

File tree

13 files changed

+1488
-7
lines changed

13 files changed

+1488
-7
lines changed

plugins/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,18 @@ dependencies {
6666
implementation("com.google.code.gson:gson:2.8.9")
6767
implementation(libs.android.gradlePlugin.gradle)
6868
implementation(libs.android.gradlePlugin.builder.test.api)
69+
implementation("io.github.pdvrieze.xmlutil:serialization-jvm:0.90.3") {
70+
exclude("org.jetbrains.kotlinx", "kotlinx-serialization-json")
71+
exclude("org.jetbrains.kotlinx", "kotlinx-serialization-core")
72+
}
6973

74+
testImplementation(gradleTestKit())
7075
testImplementation(libs.bundles.kotest)
76+
testImplementation(libs.mockk)
7177
testImplementation(libs.junit)
7278
testImplementation(libs.truth)
7379
testImplementation("commons-io:commons-io:2.15.1")
80+
testImplementation(kotlin("test"))
7481
}
7582

7683
gradlePlugin {

plugins/src/main/java/com/google/firebase/gradle/plugins/BaseFirebaseLibraryPlugin.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.google.firebase.gradle.plugins
1818

1919
import com.android.build.gradle.LibraryExtension
2020
import com.google.firebase.gradle.plugins.ci.Coverage
21+
import com.google.firebase.gradle.plugins.services.GMavenService
2122
import java.io.File
2223
import java.nio.file.Paths
2324
import org.gradle.api.Plugin
@@ -52,6 +53,7 @@ import org.w3c.dom.Element
5253
abstract class BaseFirebaseLibraryPlugin : Plugin<Project> {
5354
protected fun setupDefaults(project: Project, library: FirebaseLibraryExtension) {
5455
with(library) {
56+
project.gradle.sharedServices.registerIfAbsent<GMavenService, _>("gmaven")
5557
previewMode.convention("")
5658
publishJavadoc.convention(true)
5759
artifactId.convention(project.name)

plugins/src/main/java/com/google/firebase/gradle/plugins/GradleExtensions.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ import org.gradle.api.attributes.Attribute
3030
import org.gradle.api.attributes.AttributeContainer
3131
import org.gradle.api.plugins.PluginManager
3232
import org.gradle.api.provider.Provider
33+
import org.gradle.api.services.BuildService
34+
import org.gradle.api.services.BuildServiceParameters
35+
import org.gradle.api.services.BuildServiceRegistry
36+
import org.gradle.api.services.BuildServiceSpec
3337
import org.gradle.kotlin.dsl.apply
34-
import org.gradle.kotlin.dsl.provideDelegate
3538
import org.gradle.workers.WorkAction
3639
import org.gradle.workers.WorkParameters
3740
import org.gradle.workers.WorkQueue
@@ -244,3 +247,19 @@ fun LibraryAndroidComponentsExtension.onReleaseVariants(
244247
) {
245248
onVariants(selector().withBuildType("release"), callback)
246249
}
250+
251+
/**
252+
* Register a build service under the specified [name], if it hasn't been registered already.
253+
*
254+
* ```
255+
* project.gradle.sharedServices.registerIfAbsent<GMavenService, _>("gmaven")
256+
* ```
257+
*
258+
* @param T The build service class to register
259+
* @param P The parameters class for the build service to register
260+
* @param name The name to register the build service under
261+
* @param config An optional configuration block to setup the build service with
262+
*/
263+
inline fun <reified T : BuildService<P>, reified P : BuildServiceParameters> BuildServiceRegistry
264+
.registerIfAbsent(name: String, noinline config: BuildServiceSpec<P>.() -> Unit = {}) =
265+
registerIfAbsent(name, T::class.java, config)

plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package com.google.firebase.gradle.plugins
1818

1919
import java.io.File
20+
import java.io.InputStream
2021
import org.w3c.dom.Element
22+
import org.w3c.dom.Node
2123
import org.w3c.dom.NodeList
2224

2325
/** Replaces all matching substrings with an empty string (nothing) */
@@ -124,6 +126,13 @@ fun Element.findOrCreate(tag: String): Element =
124126
fun Element.findElementsByTag(tag: String) =
125127
getElementsByTagName(tag).children().mapNotNull { it as? Element }
126128

129+
/**
130+
* Returns the text of an attribute, if it exists.
131+
*
132+
* @param name The name of the attribute to get the text for
133+
*/
134+
fun Node.textByAttributeOrNull(name: String) = attributes?.getNamedItem(name)?.textContent
135+
127136
/**
128137
* Yields the items of this [NodeList] as a [Sequence].
129138
*
@@ -267,6 +276,19 @@ infix fun <T> List<T>.diff(other: List<T>): List<Pair<T?, T?>> {
267276
*/
268277
fun <T> List<T>.coerceToSize(targetSize: Int) = List(targetSize) { getOrNull(it) }
269278

279+
/**
280+
* Writes the [InputStream] to this file.
281+
*
282+
* While this method _does_ close the generated output stream, it's the callers responsibility to
283+
* close the passed [stream].
284+
*
285+
* @return This [File] instance for chaining.
286+
*/
287+
fun File.writeStream(stream: InputStream): File {
288+
outputStream().use { stream.copyTo(it) }
289+
return this
290+
}
291+
270292
/**
271293
* The [path][File.path] represented as a qualified unix path.
272294
*

plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ enum class PreReleaseVersionType {
5757
* Where `Type` is a case insensitive string of any [PreReleaseVersionType], and `Build` is a two
5858
* digit number (single digits should have a leading zero).
5959
*
60-
* Note that `build` will always be present as starting at one by defalt. That is, the following
60+
* Note that `build` will always be present as starting at one by default. That is, the following
6161
* transform occurs:
6262
* ```
6363
* "12.13.1-beta" // 12.13.1-beta01
@@ -92,7 +92,7 @@ data class PreReleaseVersion(val type: PreReleaseVersionType, val build: Int = 1
9292
*/
9393
fun fromStringsOrNull(type: String, build: String): PreReleaseVersion? =
9494
runCatching {
95-
val preType = PreReleaseVersionType.valueOf(type.toUpperCase())
95+
val preType = PreReleaseVersionType.valueOf(type.uppercase())
9696
val buildNumber = build.takeUnless { it.isBlank() }?.toInt() ?: 1
9797

9898
PreReleaseVersion(preType, buildNumber)
@@ -115,7 +115,7 @@ data class PreReleaseVersion(val type: PreReleaseVersionType, val build: Int = 1
115115
* PreReleaseVersion(RC, 12).toString() // "rc12"
116116
* ```
117117
*/
118-
override fun toString() = "${type.name.toLowerCase()}${build.toString().padStart(2, '0')}"
118+
override fun toString() = "${type.name.lowercase()}${build.toString().padStart(2, '0')}"
119119
}
120120

121121
/**
@@ -140,7 +140,7 @@ data class ModuleVersion(
140140
) : Comparable<ModuleVersion> {
141141

142142
/** Formatted as `MAJOR.MINOR.PATCH-PRE` */
143-
override fun toString() = "$major.$minor.$patch${pre?.let { "-${it.toString()}" } ?: ""}"
143+
override fun toString() = "$major.$minor.$patch${pre?.let { "-$it" } ?: ""}"
144144

145145
override fun compareTo(other: ModuleVersion) =
146146
compareValuesBy(
@@ -149,7 +149,7 @@ data class ModuleVersion(
149149
{ it.major },
150150
{ it.minor },
151151
{ it.patch },
152-
{ it.pre == null }, // a version with no prerelease version takes precedence
152+
{ it.pre == null }, // a version with no pre-release version takes precedence
153153
{ it.pre },
154154
)
155155

@@ -176,7 +176,7 @@ data class ModuleVersion(
176176
* ```
177177
*/
178178
val VERSION_REGEX =
179-
"(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(?:\\-\\b)?(?<pre>\\w\\D+)?(?<build>\\B\\d+)?"
179+
"(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(?:-\\b)?(?<pre>\\w\\D+)?(?<build>\\B\\d+)?"
180180
.toRegex()
181181

182182
/**
@@ -209,6 +209,29 @@ data class ModuleVersion(
209209
}
210210
}
211211
.getOrNull()
212+
213+
/**
214+
* Parse a [ModuleVersion] from a string.
215+
*
216+
* You should use [fromStringOrNull] when you don't know the `artifactId` of the corresponding
217+
* artifact, if you don't need to throw on failure, or if you need to throw a more specific
218+
* message.
219+
*
220+
* This method exists to cover the common ground of getting [ModuleVersion] representations of
221+
* artifacts.
222+
*
223+
* @param artifactId The artifact that this version belongs to. Will be used in the error
224+
* message on failure.
225+
* @param version The version to parse into a [ModuleVersion].
226+
* @return A [ModuleVersion] created from the string.
227+
* @throws IllegalArgumentException If the string doesn't represent a valid semver version.
228+
* @see fromStringOrNull
229+
*/
230+
fun fromString(artifactId: String, version: String): ModuleVersion =
231+
fromStringOrNull(version)
232+
?: throw IllegalArgumentException(
233+
"Invalid module version found for '${artifactId}': $version"
234+
)
212235
}
213236

214237
/**
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright 2025 Google LLC
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 com.google.firebase.gradle.plugins.datamodels
18+
19+
import com.google.firebase.gradle.plugins.ModuleVersion
20+
import java.io.File
21+
import javax.xml.parsers.DocumentBuilderFactory
22+
import kotlinx.serialization.Serializable
23+
import kotlinx.serialization.encodeToString
24+
import nl.adaptivity.xmlutil.XmlDeclMode
25+
import nl.adaptivity.xmlutil.newReader
26+
import nl.adaptivity.xmlutil.serialization.XML
27+
import nl.adaptivity.xmlutil.serialization.XmlChildrenName
28+
import nl.adaptivity.xmlutil.serialization.XmlElement
29+
import nl.adaptivity.xmlutil.serialization.XmlSerialName
30+
import nl.adaptivity.xmlutil.xmlStreaming
31+
import org.w3c.dom.Element
32+
33+
/**
34+
* Representation of a `<license />` element in a a pom file.
35+
*
36+
* @see PomElement
37+
*/
38+
@Serializable
39+
@XmlSerialName("license")
40+
data class LicenseElement(
41+
@XmlElement val name: String,
42+
@XmlElement val url: String? = null,
43+
@XmlElement val distribution: String? = null,
44+
)
45+
46+
/**
47+
* Representation of an `<scm />` element in a a pom file.
48+
*
49+
* @see PomElement
50+
*/
51+
@Serializable
52+
@XmlSerialName("scm")
53+
data class SourceControlManagement(@XmlElement val connection: String, @XmlElement val url: String)
54+
55+
/**
56+
* Representation of a `<dependency />` element in a pom file.
57+
*
58+
* @see PomElement
59+
*/
60+
@Serializable
61+
@XmlSerialName("dependency")
62+
data class ArtifactDependency(
63+
@XmlElement val groupId: String,
64+
@XmlElement val artifactId: String,
65+
// Can be null if the artifact derives its version from a bom
66+
@XmlElement val version: String? = null,
67+
@XmlElement val type: String? = null,
68+
@XmlElement val scope: String? = null,
69+
) {
70+
/**
71+
* Returns the artifact dependency as a a gradle dependency string.
72+
*
73+
* ```
74+
* implementation("com.google.firebase:firebase-firestore:1.0.0")
75+
* ```
76+
*
77+
* @see configuration
78+
* @see simpleDepString
79+
*/
80+
override fun toString() = "$configuration(\"$simpleDepString\")"
81+
}
82+
83+
/**
84+
* The artifact type of this dependency, or the default inferred by gradle.
85+
*
86+
* We use a separate variable instead of inferring the default in the constructor so we can
87+
* serialize instances of [ArtifactDependency] that should specifically _not_ have a type in the
88+
* output (like in [DependencyManagementElement] instances).
89+
*/
90+
val ArtifactDependency.typeOrDefault: String
91+
get() = type ?: "jar"
92+
93+
/**
94+
* The artifact scope of this dependency, or the default inferred by gradle.
95+
*
96+
* We use a separate variable instead of inferring the default in the constructor so we can
97+
* serialize instances of [ArtifactDependency] that should specifically _not_ have a scope in the
98+
* output (like in [DependencyManagementElement] instances).
99+
*/
100+
val ArtifactDependency.scopeOrDefault: String
101+
get() = scope ?: "compile"
102+
103+
/**
104+
* The [version][ArtifactDependency.version] represented as a [ModuleVersion].
105+
*
106+
* @throws RuntimeException if the version isn't valid semver, or it's missing.
107+
*/
108+
val ArtifactDependency.moduleVersion: ModuleVersion
109+
get() =
110+
version?.let { ModuleVersion.fromString(artifactId, it) }
111+
?: throw RuntimeException(
112+
"Missing required version property for artifact dependency: $artifactId"
113+
)
114+
115+
/**
116+
* The fully qualified name of the artifact.
117+
*
118+
* Shorthand for:
119+
* ```
120+
* "${artifact.groupId}:${artifact.artifactId}"
121+
* ```
122+
*/
123+
val ArtifactDependency.fullArtifactName: String
124+
get() = "$groupId:$artifactId"
125+
126+
/**
127+
* A string representing the dependency as a maven artifact marker.
128+
*
129+
* ```
130+
* "com.google.firebase:firebase-common:21.0.0"
131+
* ```
132+
*/
133+
val ArtifactDependency.simpleDepString: String
134+
get() = "$fullArtifactName${version?.let { ":$it" } ?: ""}"
135+
136+
/** The gradle configuration that this dependency would apply to (eg; `api` or `implementation`). */
137+
val ArtifactDependency.configuration: String
138+
get() = if (scopeOrDefault == "compile") "api" else "implementation"
139+
140+
@Serializable
141+
@XmlSerialName("dependencyManagement")
142+
data class DependencyManagementElement(
143+
@XmlChildrenName("dependency") val dependencies: List<ArtifactDependency>? = null
144+
)
145+
146+
/** Representation of a `<project />` element within a `pom.xml` file. */
147+
@Serializable
148+
@XmlSerialName("project")
149+
data class PomElement(
150+
@XmlSerialName("xmlns") val namespace: String? = null,
151+
@XmlSerialName("xmlns:xsi") val schema: String? = null,
152+
@XmlSerialName("xsi:schemaLocation") val schemaLocation: String? = null,
153+
@XmlElement val modelVersion: String,
154+
@XmlElement val groupId: String,
155+
@XmlElement val artifactId: String,
156+
@XmlElement val version: String,
157+
@XmlElement val packaging: String? = null,
158+
@XmlChildrenName("licenses") val licenses: List<LicenseElement>? = null,
159+
@XmlElement val scm: SourceControlManagement? = null,
160+
@XmlElement val dependencyManagement: DependencyManagementElement? = null,
161+
@XmlChildrenName("dependency") val dependencies: List<ArtifactDependency>? = null,
162+
) {
163+
/**
164+
* Serializes this pom element into a valid XML element and saves it to the specified [file].
165+
*
166+
* @param file Where to save the serialized pom to
167+
* @return The provided file, for chaining purposes.
168+
* @see fromFile
169+
*/
170+
fun toFile(file: File): File {
171+
val xmlWriter = XML {
172+
indent = 2
173+
xmlDeclMode = XmlDeclMode.None
174+
}
175+
file.writeText(xmlWriter.encodeToString(this))
176+
return file
177+
}
178+
179+
companion object {
180+
/**
181+
* Deserializes a [PomElement] from a `pom.xml` file.
182+
*
183+
* @param file The file that contains the pom element.
184+
* @return The deserialized [PomElement]
185+
* @see toFile
186+
* @see fromElement
187+
*/
188+
fun fromFile(file: File): PomElement =
189+
fromElement(
190+
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file).documentElement
191+
)
192+
193+
/**
194+
* Deserializes a [PomElement] from a document [Element].
195+
*
196+
* @param element The HTML element representing the pom element.
197+
* @return The deserialized [PomElement]
198+
* @see fromFile
199+
*/
200+
fun fromElement(element: Element): PomElement =
201+
XML.decodeFromReader(xmlStreaming.newReader(element))
202+
}
203+
}

0 commit comments

Comments
 (0)