diff --git a/firebase-dataconnect/connectors/connectors.gradle.kts b/firebase-dataconnect/connectors/connectors.gradle.kts index 358f8227888..c473ab612f9 100644 --- a/firebase-dataconnect/connectors/connectors.gradle.kts +++ b/firebase-dataconnect/connectors/connectors.gradle.kts @@ -15,6 +15,7 @@ */ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import com.google.firebase.dataconnect.gradle.plugin.UpdateDataConnectExecutableVersionsTask plugins { id("com.android.library") @@ -109,3 +110,41 @@ tasks.withType().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("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? = 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")) +} \ No newline at end of file diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/UpdateDataConnectExecutableVersionsTask.kt b/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/UpdateDataConnectExecutableVersionsTask.kt new file mode 100644 index 00000000000..27f1eb300a4 --- /dev/null +++ b/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/UpdateDataConnectExecutableVersionsTask.kt @@ -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 + + @get:Input @get:Optional abstract val defaultVersion: Property + + @get:Input @get:Optional abstract val updateMode: Property + + @get:Internal abstract val workDirectory: DirectoryProperty + + @TaskAction + fun run() { + val jsonFile: File = jsonFile.get().asFile + val versions: List = 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 = "", + 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 + } +} diff --git a/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/Util.kt b/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/Util.kt index 1fe3b5e57ce..bc0904a5570 100644 --- a/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/Util.kt +++ b/firebase-dataconnect/gradleplugin/plugin/src/main/kotlin/com/google/firebase/dataconnect/gradle/plugin/Util.kt @@ -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 @@ -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"