Skip to content

Commit

Permalink
dataconnect: add firebase-dataconnect:connectors:updateJson task (#6358)
Browse files Browse the repository at this point in the history
  • Loading branch information
dconeybe authored Oct 9, 2024
1 parent 18fd9f7 commit b49d448
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 0 deletions.
39 changes: 39 additions & 0 deletions firebase-dataconnect/connectors/connectors.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.google.firebase.dataconnect.gradle.plugin.UpdateDataConnectExecutableVersionsTask

plugins {
id("com.android.library")
Expand Down Expand Up @@ -109,3 +110,41 @@ tasks.withType<KotlinCompile>().all {
}
}
}

// Adds a Gradle task that updates the JSON file that stores the list of Data Connect
// executable versions.
//
// Example 1: Add versions 1.4.3 and 1.4.4 to the JSON file, and set 1.4.4 as the default:
// ../../gradlew -Pversions=1.4.3,1.4.4 -PdefaultVersion=1.4.4 updateJson --info
//
// Example 2: Add version 1.2.3 to the JSON file, but do not change the default version:
// ../../gradlew -Pversion=1.2.3 updateJson --info
//
// The `--info` argument can be omitted; it merely controls the level of log output.
tasks.register<UpdateDataConnectExecutableVersionsTask>("updateJson") {
outputs.upToDateWhen { false }
jsonFile.set(project.layout.projectDirectory.file(
"../gradleplugin/plugin/src/main/resources/com/google/firebase/dataconnect/gradle/" +
"plugin/DataConnectExecutableVersions.json"))
workDirectory.set(project.layout.buildDirectory.dir("updateJson"))

val singleVersion: String? = project.providers.gradleProperty("version").orNull
val multipleVersions: List<String>? = project.providers.gradleProperty("versions").orNull?.split(',')
versions.set(buildList {
singleVersion?.let{add(it)}
multipleVersions?.let{addAll(it)}
if (isEmpty()) {
throw Exception("bm6d5ezxzd 'version' or 'versions' property must be specified")
}
})

updateMode.set(project.providers.gradleProperty("updateMode").map {
when (it) {
"overwrite" -> UpdateDataConnectExecutableVersionsTask.UpdateMode.Overwrite
"update" -> UpdateDataConnectExecutableVersionsTask.UpdateMode.Update
else -> throw Exception("ahe4zadcjs 'updateMode' must be 'overwrite' or 'update', but got: $it")
}
})

defaultVersion.set(project.providers.gradleProperty("defaultVersion"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2024 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.dataconnect.gradle.plugin

import com.google.firebase.dataconnect.gradle.plugin.DataConnectExecutableDownloadTask.Companion.downloadDataConnectExecutable
import com.google.firebase.dataconnect.gradle.plugin.DataConnectExecutableDownloadTask.FileInfo
import java.io.File
import kotlin.random.Random
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction

@Suppress("unused")
abstract class UpdateDataConnectExecutableVersionsTask : DefaultTask() {

@get:InputFile abstract val jsonFile: RegularFileProperty

@get:Input abstract val versions: ListProperty<String>

@get:Input @get:Optional abstract val defaultVersion: Property<String>

@get:Input @get:Optional abstract val updateMode: Property<UpdateMode>

@get:Internal abstract val workDirectory: DirectoryProperty

@TaskAction
fun run() {
val jsonFile: File = jsonFile.get().asFile
val versions: List<String> = versions.get()
val defaultVersion: String? = defaultVersion.orNull
val updateMode: UpdateMode? = updateMode.orNull
val workDirectory: File = workDirectory.get().asFile

logger.info("jsonFile={}", jsonFile.absolutePath)
logger.info("versions={}", versions)
logger.info("defaultVersion={}", defaultVersion)
logger.info("updateMode={}", updateMode)
logger.info("workDirectory={}", workDirectory)

var json: DataConnectExecutableVersionsRegistry.Root =
if (updateMode == UpdateMode.Overwrite) {
DataConnectExecutableVersionsRegistry.Root(
defaultVersion = "<unspecified>",
versions = emptyList()
)
} else {
logger.info("Loading JSON file {}", jsonFile.absolutePath)
DataConnectExecutableVersionsRegistry.load(jsonFile)
}

if (defaultVersion !== null) {
json = json.copy(defaultVersion = defaultVersion)
}

for (version in versions) {
val windowsExecutable = download(version, OperatingSystem.Windows, workDirectory)
val macosExecutable = download(version, OperatingSystem.MacOS, workDirectory)
val linuxExecutable = download(version, OperatingSystem.Linux, workDirectory)
json = json.withVersions(version, windowsExecutable, macosExecutable, linuxExecutable)
}

logger.info(
"Writing information about versions {} to file with updateMode={}: {}",
versions.joinToString(", "),
updateMode,
jsonFile.absolutePath
)
DataConnectExecutableVersionsRegistry.save(json, jsonFile)
}

private fun DataConnectExecutableVersionsRegistry.Root.withVersions(
version: String,
windows: DownloadedFile,
macos: DownloadedFile,
linux: DownloadedFile
): DataConnectExecutableVersionsRegistry.Root {
data class UpdatedVersion(
val operatingSystem: OperatingSystem,
val sizeInBytes: Long,
val sha512DigestHex: String,
) {
constructor(
operatingSystem: OperatingSystem,
downloadedFile: DownloadedFile
) : this(operatingSystem, downloadedFile.sizeInBytes, downloadedFile.sha512DigestHex)
}
val updatedVersions =
listOf(
UpdatedVersion(OperatingSystem.Windows, windows),
UpdatedVersion(OperatingSystem.MacOS, macos),
UpdatedVersion(OperatingSystem.Linux, linux),
)

val newVersions = versions.toMutableList()
for (updatedVersion in updatedVersions) {
val index =
newVersions.indexOfFirst {
it.version == version && it.os == updatedVersion.operatingSystem
}
if (index >= 0) {
val newVersion =
newVersions[index].copy(
size = updatedVersion.sizeInBytes,
sha512DigestHex = updatedVersion.sha512DigestHex,
)
newVersions[index] = newVersion
} else {
val newVersion =
DataConnectExecutableVersionsRegistry.VersionInfo(
version = version,
os = updatedVersion.operatingSystem,
size = updatedVersion.sizeInBytes,
sha512DigestHex = updatedVersion.sha512DigestHex,
)
newVersions.add(newVersion)
}
}

return this.copy(versions = newVersions.toList())
}

private fun download(
version: String,
operatingSystem: OperatingSystem,
outputDirectory: File
): DownloadedFile {
val randomId = Random.nextAlphanumericString(length = 20)
val outputFile =
File(outputDirectory, "DataConnectToolkit_${version}_${operatingSystem}_$randomId")

downloadDataConnectExecutable(version, operatingSystem, outputFile)

logger.info("Calculating SHA512 hash of file: {}", outputFile.absolutePath)
val fileInfo = FileInfo.forFile(outputFile)

return DownloadedFile(
file = outputFile,
sizeInBytes = fileInfo.sizeInBytes,
sha512DigestHex = fileInfo.sha512DigestHex,
)
}

private data class DownloadedFile(
val file: File,
val sizeInBytes: Long,
val sha512DigestHex: String,
)

enum class UpdateMode {
Overwrite,
Update
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package com.google.firebase.dataconnect.gradle.plugin

import java.util.Locale
import java.util.concurrent.atomic.AtomicLong
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
Expand Down Expand Up @@ -46,3 +47,24 @@ class Debouncer(val period: Duration) {
}
}
}

/**
* Generates and returns a string containing random alphanumeric characters.
*
* The characters returned are taken from the set of characters comprising of the 10 numeric digits
* and the 26 lowercase English characters.
*
* @param length the number of random characters to generate and include in the returned string;
* must be greater than or equal to zero.
* @return a string containing the given number of random alphanumeric characters.
*/
fun Random.nextAlphanumericString(length: Int): String {
require(length >= 0) { "invalid length: $length" }
return (0 until length).map { ALPHANUMERIC_ALPHABET.random(this) }.joinToString(separator = "")
}

// The set of characters consisting of the 10 numeric digits and the 26 lowercase letters of the
// English alphabet with some characters removed that can look similar in different fonts, like
// '1', 'l', and 'i'.
@Suppress("SpellCheckingInspection")
private const val ALPHANUMERIC_ALPHABET = "23456789abcdefghjkmnpqrstvwxyz"

0 comments on commit b49d448

Please sign in to comment.