Skip to content

Commit b8803fc

Browse files
authored
Refactor bom generation (#6675)
Per [b/394616465](https://b.corp.google.com/issues/394616465), This refactors our bom generation and all of the bom related tasks to solve the following issues: 1. Testability. 2. Gradle cache and config avoidance compliance. 3. Generation of bom, bom release notes, and tutorial bundle in isolation of each other. 4. Documentation. 5. Usage of classes that were only used for bom generation (of which, suitable replacements were used elsewhere in our plugin logic). Effectively, this PR splits bom generation into 3 tasks: `GenerateBomTask`, `GenerateBomReleaseNotesTask`, and `GenerateTutorialBundleTask`. These 3 tasks get ran together during the release, but them being separated now makes it easier to not only run them in isolation of each other- but test them in isolation. As such, this PR also includes tests for all of the bom related tasks, and documentation for everything. These tasks also take advantage of the new `GMavenService`- to support proper parallel execution and caching. And finally, the configuration of these tasks has been moved to the use-site instead of the declaration site, to more cleanly match other tasks and the configuration style that the Gradle team pushes. This PR also fixes the following: - [b/394614707](https://b.corp.google.com/issues/394614707) -> Move tutorial bundle generation to its own task - [b/394614708](https://b.corp.google.com/issues/394614708) -> Move bom release note generation to its own task - [b/394614709](https://b.corp.google.com/issues/394614709) -> Create a new task for bom generation - [b/394615027](https://b.corp.google.com/issues/394615027) -> Add tests for bom related tasks
1 parent 92f448f commit b8803fc

22 files changed

+2266
-1134
lines changed

plugins/src/main/java/com/google/firebase/gradle/bomgenerator/BomGeneratorTask.java

Lines changed: 0 additions & 360 deletions
This file was deleted.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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.bomgenerator
18+
19+
import com.google.firebase.gradle.plugins.createIfAbsent
20+
import com.google.firebase.gradle.plugins.datamodels.ArtifactDependency
21+
import com.google.firebase.gradle.plugins.datamodels.PomElement
22+
import com.google.firebase.gradle.plugins.datamodels.fullArtifactName
23+
import org.gradle.api.DefaultTask
24+
import org.gradle.api.file.RegularFileProperty
25+
import org.gradle.api.provider.MapProperty
26+
import org.gradle.api.provider.Property
27+
import org.gradle.api.tasks.Input
28+
import org.gradle.api.tasks.InputFile
29+
import org.gradle.api.tasks.Internal
30+
import org.gradle.api.tasks.OutputFile
31+
import org.gradle.api.tasks.TaskAction
32+
33+
/**
34+
* Generates the release notes for a bom.
35+
*
36+
* @see GenerateBomTask
37+
*/
38+
abstract class GenerateBomReleaseNotesTask : DefaultTask() {
39+
@get:InputFile abstract val currentBom: RegularFileProperty
40+
41+
@get:Input abstract val previousBom: Property<PomElement>
42+
43+
@get:OutputFile abstract val releaseNotesFile: RegularFileProperty
44+
45+
@get:Internal abstract val previousBomVersions: MapProperty<String, String?>
46+
47+
@TaskAction
48+
fun generate() {
49+
val bom = PomElement.fromFile(currentBom.asFile.get())
50+
val currentDeps = bom.dependencyManagement?.dependencies.orEmpty()
51+
val previousDeps = previousBom.get().dependencyManagement?.dependencies.orEmpty()
52+
previousBomVersions.set(previousDeps.associate { it.fullArtifactName to it.version })
53+
54+
val sortedDependencies = currentDeps.sortedBy { it.version }
55+
56+
val headingId = "{: #bom_v${bom.version.replace(".", "-")}}"
57+
58+
releaseNotesFile.asFile
59+
.get()
60+
.createIfAbsent()
61+
.writeText(
62+
"""
63+
|### {{firebase_bom_long}} ({{bill_of_materials}}) version ${bom.version} $headingId
64+
|{% comment %}
65+
|These library versions must be flat-typed, do not use variables.
66+
|The release note for this BoM version is a library-version snapshot.
67+
|{% endcomment %}
68+
|
69+
|<section class="expandable">
70+
| <p class="showalways">
71+
| Firebase Android SDKs mapped to this {{bom}} version
72+
| </p>
73+
| <p>
74+
| Libraries that were versioned with this release are in highlighted rows.<br>
75+
| Refer to a library's release notes (on this page) for details about its changes.
76+
| </p>
77+
| <table>
78+
| <thead>
79+
| <th>Artifact name</th>
80+
| <th>Version mapped<br>to previous {{bom}} v${previousBom.get().version}</th>
81+
| <th>Version mapped<br>to this {{bom}} v${bom.version}</th>
82+
| </thead>
83+
| <tbody>
84+
|${sortedDependencies.joinToString("\n") { artifactToListEntry(it) }.prependIndent(" ")}
85+
| </tbody>
86+
| </table>
87+
|</section>
88+
|
89+
"""
90+
.trimMargin()
91+
)
92+
}
93+
94+
private fun artifactToListEntry(artifact: ArtifactDependency): String {
95+
val previousVersion = previousBomVersions.get()[artifact.fullArtifactName] ?: "N/A"
96+
val artifactName = "${artifact.groupId}:${artifact.artifactId}"
97+
98+
return if (artifact.version != previousVersion) {
99+
"""
100+
|<tr class="alt">
101+
| <td><b>${artifactName}</b></td>
102+
| <td><b>$previousVersion</b></td>
103+
| <td><b>${artifact.version}</b></td>
104+
|</tr>
105+
"""
106+
.trimMargin()
107+
} else {
108+
"""
109+
|<tr>
110+
| <td>${artifactName}</td>
111+
| <td>$previousVersion</td>
112+
| <td>${artifact.version}</td>
113+
|</tr>
114+
"""
115+
.trimMargin()
116+
}
117+
}
118+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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.bomgenerator
18+
19+
import com.google.firebase.gradle.plugins.ModuleVersion
20+
import com.google.firebase.gradle.plugins.VersionType
21+
import com.google.firebase.gradle.plugins.createIfAbsent
22+
import com.google.firebase.gradle.plugins.datamodels.ArtifactDependency
23+
import com.google.firebase.gradle.plugins.datamodels.DependencyManagementElement
24+
import com.google.firebase.gradle.plugins.datamodels.LicenseElement
25+
import com.google.firebase.gradle.plugins.datamodels.PomElement
26+
import com.google.firebase.gradle.plugins.datamodels.fullArtifactName
27+
import com.google.firebase.gradle.plugins.datamodels.moduleVersion
28+
import com.google.firebase.gradle.plugins.diff
29+
import com.google.firebase.gradle.plugins.orEmpty
30+
import com.google.firebase.gradle.plugins.partitionNotNull
31+
import com.google.firebase.gradle.plugins.services.GMavenService
32+
import org.gradle.api.DefaultTask
33+
import org.gradle.api.file.DirectoryProperty
34+
import org.gradle.api.provider.ListProperty
35+
import org.gradle.api.provider.MapProperty
36+
import org.gradle.api.provider.Property
37+
import org.gradle.api.services.ServiceReference
38+
import org.gradle.api.tasks.Input
39+
import org.gradle.api.tasks.OutputDirectory
40+
import org.gradle.api.tasks.TaskAction
41+
42+
/**
43+
* Generates the firebase bom, using gmaven as a source of truth for artifacts and versions.
44+
*
45+
* @see validateArtifacts
46+
* @see GenerateBomReleaseNotesTask
47+
* @see GenerateTutorialBundleTask
48+
*/
49+
abstract class GenerateBomTask : DefaultTask() {
50+
/**
51+
* Artifacts to include in the bom.
52+
*
53+
* ```
54+
* bomArtifacts.set(listOf(
55+
* "com.google.firebase:firebase-firestore",
56+
* "com.google.firebase:firebase-storage"
57+
* ))
58+
* ```
59+
*/
60+
@get:Input abstract val bomArtifacts: ListProperty<String>
61+
62+
/**
63+
* Artifacts to exclude from the bom.
64+
*
65+
* These are artifacts that are under the `com.google.firebase` namespace, but are intentionally
66+
* not included in the bom.
67+
*
68+
* ```
69+
* bomArtifacts.set(listOf(
70+
* "com.google.firebase:crashlytics",
71+
* "com.google.firebase:crash-plugin"
72+
* ))
73+
* ```
74+
*/
75+
@get:Input abstract val ignoredArtifacts: ListProperty<String>
76+
77+
/**
78+
* Optional map of versions to use instead of the versions on gmaven.
79+
*
80+
* ```
81+
* versionOverrides.set(mapOf(
82+
* "com.google.firebase:firebase-firestore" to "10.0.0"
83+
* ))
84+
* ```
85+
*/
86+
@get:Input abstract val versionOverrides: MapProperty<String, String>
87+
88+
/** Directory to save the bom under. */
89+
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
90+
91+
@get:ServiceReference("gmaven") abstract val gmaven: Property<GMavenService>
92+
93+
@TaskAction
94+
fun generate() {
95+
val versionOverrides = versionOverrides.getOrElse(emptyMap())
96+
97+
val validatedArtifactsToPublish = validateArtifacts()
98+
val artifactsToPublish =
99+
validatedArtifactsToPublish.map {
100+
val version = versionOverrides[it.fullArtifactName] ?: it.version
101+
logger.debug("Using ${it.fullArtifactName} with version $version")
102+
103+
it.copy(version = version)
104+
}
105+
106+
val newVersion = determineNewBomVersion(artifactsToPublish)
107+
108+
val pom =
109+
PomElement(
110+
namespace = "http://maven.apache.org/POM/4.0.0",
111+
schema = "http://www.w3.org/2001/XMLSchema-instance",
112+
schemaLocation =
113+
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd",
114+
modelVersion = "4.0.0",
115+
groupId = "com.google.firebase",
116+
artifactId = "firebase-bom",
117+
version = newVersion.toString(),
118+
packaging = "pom",
119+
licenses =
120+
listOf(
121+
LicenseElement(
122+
name = "The Apache Software License, Version 2.0",
123+
url = "http://www.apache.org/licenses/LICENSE-2.0.txt",
124+
distribution = "repo",
125+
)
126+
),
127+
dependencyManagement = DependencyManagementElement(artifactsToPublish),
128+
)
129+
130+
val bomFile =
131+
outputDirectory.file(
132+
"com/google/firebase/firebase-bom/$newVersion/firebase-bom-$newVersion.pom"
133+
)
134+
135+
pom.toFile(bomFile.get().asFile.createIfAbsent())
136+
}
137+
138+
private fun determineNewBomVersion(
139+
releasingDependencies: List<ArtifactDependency>
140+
): ModuleVersion {
141+
logger.info("Determining the new bom version")
142+
143+
val oldBom = gmaven.get().latestPom("com.google.firebase", "firebase-bom")
144+
val oldBomVersion = ModuleVersion.fromString(oldBom.artifactId, oldBom.version)
145+
146+
val oldBomDependencies = oldBom.dependencyManagement?.dependencies.orEmpty()
147+
val changedDependencies = oldBomDependencies.diff(releasingDependencies)
148+
149+
val versionBumps =
150+
changedDependencies.mapNotNull { (old, new) ->
151+
if (old == null) {
152+
logger.warn("Dependency was added: ${new?.fullArtifactName}")
153+
154+
VersionType.MINOR
155+
} else if (new === null) {
156+
logger.warn("Dependency was removed: ${old.fullArtifactName}")
157+
158+
VersionType.MAJOR
159+
} else {
160+
old.moduleVersion.bumpFrom(new.moduleVersion)
161+
}
162+
}
163+
164+
val finalBump = versionBumps.minOrNull()
165+
return oldBomVersion.bump(finalBump)
166+
}
167+
168+
/**
169+
* Validates that the provided bom artifacts satisfy the following constraints:
170+
* - All are released and live on gmaven.
171+
* - They include _all_ of the firebase artifacts on gmaven, unless they're specified in
172+
* [ignoredArtifacts].+
173+
*
174+
* @return The validated artifacts to release.
175+
* @throws RuntimeException If any of the validations fail.
176+
*/
177+
private fun validateArtifacts(): List<ArtifactDependency> {
178+
logger.info("Validating bom artifacts")
179+
180+
val firebaseArtifacts = bomArtifacts.get().toSet()
181+
val ignoredArtifacts = ignoredArtifacts.orEmpty().toSet()
182+
183+
val allFirebaseArtifacts =
184+
gmaven
185+
.get()
186+
.groupIndex("com.google.firebase")
187+
.map { "${it.groupId}:${it.artifactId}" }
188+
.toSet()
189+
190+
val (released, unreleased) =
191+
firebaseArtifacts
192+
.associateWith { gmaven.get().groupIndexArtifactOrNull(it) }
193+
.partitionNotNull()
194+
195+
if (unreleased.isNotEmpty()) {
196+
throw RuntimeException(
197+
"""
198+
|Some artifacts required for bom generation are not live on gmaven yet:
199+
|${unreleased.joinToString("\n")}
200+
"""
201+
.trimMargin()
202+
)
203+
}
204+
205+
val requiredArtifacts = allFirebaseArtifacts - ignoredArtifacts
206+
val missingArtifacts = requiredArtifacts - firebaseArtifacts
207+
if (missingArtifacts.isNotEmpty()) {
208+
throw RuntimeException(
209+
"""
210+
|There are Firebase artifacts missing from the provided bom artifacts.
211+
|Add the artifacts to the ignoredArtifacts property to ignore them or to the bomArtifacts property to include them in the bom.
212+
|Dependencies missing:
213+
|${missingArtifacts.joinToString("\n")}
214+
"""
215+
.trimMargin()
216+
)
217+
}
218+
219+
return released.values.map { it.toArtifactDependency() }
220+
}
221+
}

0 commit comments

Comments
 (0)