Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions plugins/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,18 @@ dependencies {
implementation("com.google.code.gson:gson:2.8.9")
implementation(libs.android.gradlePlugin.gradle)
implementation(libs.android.gradlePlugin.builder.test.api)
implementation("io.github.pdvrieze.xmlutil:serialization-jvm:0.90.3") {
exclude("org.jetbrains.kotlinx", "kotlinx-serialization-json")
exclude("org.jetbrains.kotlinx", "kotlinx-serialization-core")
}

testImplementation(gradleTestKit())
testImplementation(libs.bundles.kotest)
testImplementation(libs.mockk)
testImplementation(libs.junit)
testImplementation(libs.truth)
testImplementation("commons-io:commons-io:2.15.1")
testImplementation(kotlin("test"))
}

gradlePlugin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.google.firebase.gradle.plugins

import com.android.build.gradle.LibraryExtension
import com.google.firebase.gradle.plugins.ci.Coverage
import com.google.firebase.gradle.plugins.services.GMavenService
import java.io.File
import java.nio.file.Paths
import org.gradle.api.Plugin
Expand Down Expand Up @@ -52,6 +53,7 @@ import org.w3c.dom.Element
abstract class BaseFirebaseLibraryPlugin : Plugin<Project> {
protected fun setupDefaults(project: Project, library: FirebaseLibraryExtension) {
with(library) {
project.gradle.sharedServices.registerIfAbsent<GMavenService, _>("gmaven")
previewMode.convention("")
publishJavadoc.convention(true)
artifactId.convention(project.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.plugins.PluginManager
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.services.BuildServiceRegistry
import org.gradle.api.services.BuildServiceSpec
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkQueue
Expand Down Expand Up @@ -244,3 +247,19 @@ fun LibraryAndroidComponentsExtension.onReleaseVariants(
) {
onVariants(selector().withBuildType("release"), callback)
}

/**
* Register a build service under the specified [name], if it hasn't been registered already.
*
* ```
* project.gradle.sharedServices.registerIfAbsent<GMavenService, _>("gmaven")
* ```
*
* @param T The build service class to register
* @param P The parameters class for the build service to register
* @param name The name to register the build service under
* @param config An optional configuration block to setup the build service with
*/
inline fun <reified T : BuildService<P>, reified P : BuildServiceParameters> BuildServiceRegistry
.registerIfAbsent(name: String, noinline config: BuildServiceSpec<P>.() -> Unit = {}) =
registerIfAbsent(name, T::class.java, config)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package com.google.firebase.gradle.plugins

import java.io.File
import java.io.InputStream
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList

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

/**
* Returns the text of an attribute, if it exists.
*
* @param name The name of the attribute to get the text for
*/
fun Node.textByAttributeOrNull(name: String) = attributes?.getNamedItem(name)?.textContent

/**
* Yields the items of this [NodeList] as a [Sequence].
*
Expand Down Expand Up @@ -267,6 +276,19 @@ infix fun <T> List<T>.diff(other: List<T>): List<Pair<T?, T?>> {
*/
fun <T> List<T>.coerceToSize(targetSize: Int) = List(targetSize) { getOrNull(it) }

/**
* Writes the [InputStream] to this file.
*
* While this method _does_ close the generated output stream, it's the callers responsibility to
* close the passed [stream].
*
* @return This [File] instance for chaining.
*/
fun File.writeStream(stream: InputStream): File {
outputStream().use { stream.copyTo(it) }
return this
}

/**
* The [path][File.path] represented as a qualified unix path.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ enum class PreReleaseVersionType {
* Where `Type` is a case insensitive string of any [PreReleaseVersionType], and `Build` is a two
* digit number (single digits should have a leading zero).
*
* Note that `build` will always be present as starting at one by defalt. That is, the following
* Note that `build` will always be present as starting at one by default. That is, the following
* transform occurs:
* ```
* "12.13.1-beta" // 12.13.1-beta01
Expand Down Expand Up @@ -92,7 +92,7 @@ data class PreReleaseVersion(val type: PreReleaseVersionType, val build: Int = 1
*/
fun fromStringsOrNull(type: String, build: String): PreReleaseVersion? =
runCatching {
val preType = PreReleaseVersionType.valueOf(type.toUpperCase())
val preType = PreReleaseVersionType.valueOf(type.uppercase())
val buildNumber = build.takeUnless { it.isBlank() }?.toInt() ?: 1

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

/**
Expand All @@ -140,7 +140,7 @@ data class ModuleVersion(
) : Comparable<ModuleVersion> {

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

override fun compareTo(other: ModuleVersion) =
compareValuesBy(
Expand All @@ -149,7 +149,7 @@ data class ModuleVersion(
{ it.major },
{ it.minor },
{ it.patch },
{ it.pre == null }, // a version with no prerelease version takes precedence
{ it.pre == null }, // a version with no pre-release version takes precedence
{ it.pre },
)

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

/**
Expand Down Expand Up @@ -209,6 +209,29 @@ data class ModuleVersion(
}
}
.getOrNull()

/**
* Parse a [ModuleVersion] from a string.
*
* You should use [fromStringOrNull] when you don't know the `artifactId` of the corresponding
* artifact, if you don't need to throw on failure, or if you need to throw a more specific
* message.
*
* This method exists to cover the common ground of getting [ModuleVersion] representations of
* artifacts.
*
* @param artifactId The artifact that this version belongs to. Will be used in the error
* message on failure.
* @param version The version to parse into a [ModuleVersion].
* @return A [ModuleVersion] created from the string.
* @throws IllegalArgumentException If the string doesn't represent a valid semver version.
* @see fromStringOrNull
*/
fun fromString(artifactId: String, version: String): ModuleVersion =
fromStringOrNull(version)
?: throw IllegalArgumentException(
"Invalid module version found for '${artifactId}': $version"
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.gradle.plugins.datamodels

import com.google.firebase.gradle.plugins.ModuleVersion
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.newReader
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlChildrenName
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.xmlStreaming
import org.w3c.dom.Element

/**
* Representation of a `<license />` element in a a pom file.
*
* @see PomElement
*/
@Serializable
@XmlSerialName("license")
data class LicenseElement(
@XmlElement val name: String,
@XmlElement val url: String? = null,
@XmlElement val distribution: String? = null,
)

/**
* Representation of an `<scm />` element in a a pom file.
*
* @see PomElement
*/
@Serializable
@XmlSerialName("scm")
data class SourceControlManagement(@XmlElement val connection: String, @XmlElement val url: String)

/**
* Representation of a `<dependency />` element in a pom file.
*
* @see PomElement
*/
@Serializable
@XmlSerialName("dependency")
data class ArtifactDependency(
@XmlElement val groupId: String,
@XmlElement val artifactId: String,
// Can be null if the artifact derives its version from a bom
@XmlElement val version: String? = null,
@XmlElement val type: String? = null,
@XmlElement val scope: String? = null,
) {
/**
* Returns the artifact dependency as a a gradle dependency string.
*
* ```
* implementation("com.google.firebase:firebase-firestore:1.0.0")
* ```
*
* @see configuration
* @see simpleDepString
*/
override fun toString() = "$configuration(\"$simpleDepString\")"
}

/**
* The artifact type of this dependency, or the default inferred by gradle.
*
* We use a separate variable instead of inferring the default in the constructor so we can
* serialize instances of [ArtifactDependency] that should specifically _not_ have a type in the
* output (like in [DependencyManagementElement] instances).
*/
val ArtifactDependency.typeOrDefault: String
get() = type ?: "jar"

/**
* The artifact scope of this dependency, or the default inferred by gradle.
*
* We use a separate variable instead of inferring the default in the constructor so we can
* serialize instances of [ArtifactDependency] that should specifically _not_ have a scope in the
* output (like in [DependencyManagementElement] instances).
*/
val ArtifactDependency.scopeOrDefault: String
get() = scope ?: "compile"

/**
* The [version][ArtifactDependency.version] represented as a [ModuleVersion].
*
* @throws RuntimeException if the version isn't valid semver, or it's missing.
*/
val ArtifactDependency.moduleVersion: ModuleVersion
get() =
version?.let { ModuleVersion.fromString(artifactId, it) }
?: throw RuntimeException(
"Missing required version property for artifact dependency: $artifactId"
)

/**
* The fully qualified name of the artifact.
*
* Shorthand for:
* ```
* "${artifact.groupId}:${artifact.artifactId}"
* ```
*/
val ArtifactDependency.fullArtifactName: String
get() = "$groupId:$artifactId"

/**
* A string representing the dependency as a maven artifact marker.
*
* ```
* "com.google.firebase:firebase-common:21.0.0"
* ```
*/
val ArtifactDependency.simpleDepString: String
get() = "$fullArtifactName${version?.let { ":$it" } ?: ""}"

/** The gradle configuration that this dependency would apply to (eg; `api` or `implementation`). */
val ArtifactDependency.configuration: String
get() = if (scopeOrDefault == "compile") "api" else "implementation"

@Serializable
@XmlSerialName("dependencyManagement")
data class DependencyManagementElement(
@XmlChildrenName("dependency") val dependencies: List<ArtifactDependency>? = null
)

/** Representation of a `<project />` element within a `pom.xml` file. */
@Serializable
@XmlSerialName("project")
data class PomElement(
@XmlSerialName("xmlns") val namespace: String? = null,
@XmlSerialName("xmlns:xsi") val schema: String? = null,
@XmlSerialName("xsi:schemaLocation") val schemaLocation: String? = null,
@XmlElement val modelVersion: String,
@XmlElement val groupId: String,
@XmlElement val artifactId: String,
@XmlElement val version: String,
@XmlElement val packaging: String? = null,
@XmlChildrenName("licenses") val licenses: List<LicenseElement>? = null,
@XmlElement val scm: SourceControlManagement? = null,
@XmlElement val dependencyManagement: DependencyManagementElement? = null,
@XmlChildrenName("dependency") val dependencies: List<ArtifactDependency>? = null,
) {
/**
* Serializes this pom element into a valid XML element and saves it to the specified [file].
*
* @param file Where to save the serialized pom to
* @return The provided file, for chaining purposes.
* @see fromFile
*/
fun toFile(file: File): File {
val xmlWriter = XML {
indent = 2
xmlDeclMode = XmlDeclMode.None
}
file.writeText(xmlWriter.encodeToString(this))
return file
}

companion object {
/**
* Deserializes a [PomElement] from a `pom.xml` file.
*
* @param file The file that contains the pom element.
* @return The deserialized [PomElement]
* @see toFile
* @see fromElement
*/
fun fromFile(file: File): PomElement =
fromElement(
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file).documentElement
)

/**
* Deserializes a [PomElement] from a document [Element].
*
* @param element The HTML element representing the pom element.
* @return The deserialized [PomElement]
* @see fromFile
*/
fun fromElement(element: Element): PomElement =
XML.decodeFromReader(xmlStreaming.newReader(element))
}
}
Loading
Loading