summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-07-21 16:09:33 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-07-21 16:09:33 +0000
commit7bd99a740e903a6c16b0c40a5b34e0d5bf8b3e41 (patch)
treec908a4c8796c6ee71a7ab0b9ba203be1ca2099c3
parent372c27be732a527dd8e51a53d8762a25515b4638 (diff)
parentfcf558ee0f7bc7c6314de6079efa254474bb955c (diff)
downloadgradle-recipes-studio-2022.1.1-canary.tar.gz
Change-Id: Iff6584118c3fe8d0281141e074a5a208ad4b378f
-rw-r--r--convert-tool/.gitattributes6
-rw-r--r--convert-tool/.gitignore6
-rw-r--r--convert-tool/app/build.gradle.kts50
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/ConvertTool.kt109
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/Converter.kt64
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtils.kt124
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecipeConverter.kt164
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecursiveConverter.kt101
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ReleaseConverter.kt80
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/SourceConverter.kt52
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/WorkingCopyConverter.kt75
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/Recipe.kt50
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeMetadataParser.kt67
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeUtils.kt40
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GithubPresubmitValidator.kt33
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GradleTasksExecutor.kt51
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/InternalCIValidator.kt67
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/MinMaxCurrentAgpValidator.kt91
-rw-r--r--convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/WorkingCopyValidator.kt53
-rw-r--r--convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/AGPVersionsTest.kt40
-rw-r--r--convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtilsKtTest.kt166
-rw-r--r--convert-tool/gradle/libs.versions.toml25
-rw-r--r--convert-tool/gradle/wrapper/gradle-wrapper.jarbin0 -> 59536 bytes
-rw-r--r--convert-tool/gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xconvert-tool/gradlew234
-rw-r--r--convert-tool/gradlew.bat89
-rw-r--r--convert-tool/settings.gradle.kts9
27 files changed, 1851 insertions, 0 deletions
diff --git a/convert-tool/.gitattributes b/convert-tool/.gitattributes
new file mode 100644
index 0000000..00a51af
--- /dev/null
+++ b/convert-tool/.gitattributes
@@ -0,0 +1,6 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# These are explicitly windows files and should use crlf
+*.bat text eol=crlf
+
diff --git a/convert-tool/.gitignore b/convert-tool/.gitignore
new file mode 100644
index 0000000..0e13346
--- /dev/null
+++ b/convert-tool/.gitignore
@@ -0,0 +1,6 @@
+# Ignore Gradle project-specific cache directory
+.gradle
+.idea
+
+# Ignore Gradle build output directory
+build
diff --git a/convert-tool/app/build.gradle.kts b/convert-tool/app/build.gradle.kts
new file mode 100644
index 0000000..89388d2
--- /dev/null
+++ b/convert-tool/app/build.gradle.kts
@@ -0,0 +1,50 @@
+// workaround for https://youtrack.jetbrains.com/issue/KTIJ-19369
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+ alias(libs.plugins.kotlin)
+ application
+}
+
+dependencies {
+ // Align versions of all Kotlin components
+ implementation(platform(libs.kotlin.bom))
+ implementation(libs.kotlin.stdlib)
+
+ // app dependencies
+ implementation(libs.guava)
+ implementation(libs.kotlinx.cli)
+ implementation(libs.tomlj)
+ implementation(libs.semver)
+ implementation(libs.gradle.api)
+ runtimeOnly(libs.slf4j)
+
+ // test dependencies
+ testImplementation(libs.kotlin.test)
+ testImplementation(libs.kotlin.test.junit)
+ testImplementation(libs.google.truth)
+}
+
+application {
+ // Define the main class for the application.
+ mainClass.set("com.google.android.gradle_recipe.converter.ConvertToolKt")
+}
+
+base {
+ archivesName.set("recipes-converter")
+}
+
+tasks {
+ register("standaloneJar", Jar::class.java) {
+ archiveClassifier.set("all")
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ manifest {
+ attributes["Main-Class"] = "com.google.android.gradle_recipe.converter.ConvertToolKt"
+ }
+ from(configurations.runtimeClasspath.get()
+ .onEach { println("add from dependencies: ${it.name}") }
+ .map { if (it.isDirectory) it else zipTree(it) })
+ val sourcesMain = sourceSets.main.get()
+ sourcesMain.allSource.forEach { println("add from sources: ${it.name}") }
+ from(sourcesMain.output)
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/ConvertTool.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/ConvertTool.kt
new file mode 100644
index 0000000..1a59edf
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/ConvertTool.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter
+
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter
+import com.google.android.gradle_recipe.converter.converters.RecursiveConverter
+import com.google.android.gradle_recipe.converter.converters.convertStringToMode
+import com.google.android.gradle_recipe.converter.validators.GithubPresubmitValidator
+import com.google.android.gradle_recipe.converter.validators.InternalCIValidator
+import com.google.android.gradle_recipe.converter.validators.WorkingCopyValidator
+import kotlinx.cli.ArgParser
+import kotlinx.cli.ArgType
+import kotlinx.cli.default
+import java.nio.file.Path
+
+/**
+ * The main entry to the Converter, parser the command line arguments, and calls
+ * the relevant classes to perform the work
+ */
+fun main(args: Array<String>) {
+ val parser = ArgParser("Gradle Recipe Converter")
+
+ val action by parser.option(
+ ArgType.Choice(listOf("convert", "validate"), { it }), shortName = "al", description = "Recipe action"
+ ).default("convert")
+
+ val overwrite by parser.option(ArgType.Boolean, shortName = "o", description = "Overwrite").default(false)
+ val source by parser.option(ArgType.String, shortName = "s", description = "Recipe source")
+ val sourceAll by parser.option(ArgType.String, shortName = "sa", description = "All recipe sources")
+ val destination by parser.option(ArgType.String, shortName = "d", description = "Destination folder")
+ val tmpFolder by parser.option(ArgType.String, shortName = "tf", description = "Temp folder")
+ val agpVersion by parser.option(ArgType.String, shortName = "a", description = "AGP version")
+ val repoLocation by parser.option(ArgType.String, shortName = "rl", description = "Repo location")
+ val gradleVersion by parser.option(ArgType.String, shortName = "gv", description = "Gradle version")
+ val gradlePath by parser.option(ArgType.String, shortName = "gp", description = "Gradle path")
+ val mode by parser.option(ArgType.String, shortName = "m", description = "Mode")
+
+ parser.parse(args)
+
+ if (action == "convert") {
+ if (source != null) {
+ RecipeConverter(
+ agpVersion = agpVersion,
+ repoLocation = repoLocation,
+ gradleVersion = gradleVersion,
+ gradlePath = gradlePath,
+ mode = convertStringToMode(mode),
+ overwrite = overwrite
+ ).convert(
+ source = Path.of(source ?: error("source must be specified")),
+ destination = Path.of(destination ?: error("destination must be specified"))
+ )
+ } else {
+ RecursiveConverter(
+ agpVersion = agpVersion,
+ repoLocation = repoLocation,
+ gradleVersion = gradleVersion,
+ gradlePath = gradlePath,
+ overwrite = overwrite
+ ).convertAllRecipes(
+ sourceAll = Path.of(sourceAll ?: error("sourceAll must be specified")),
+ destination = Path.of(destination ?: error("destination must be specified"))
+ )
+ }
+ } else if (action == "validate") {
+ if (agpVersion == null) {
+ if (mode != null) {
+ val cliMode = convertStringToMode(mode)
+ if (cliMode != RecipeConverter.Mode.COPY) {
+ error("The mode value should be either \"copy\" or nothing")
+ }
+
+ val validator = WorkingCopyValidator()
+ validator.validate(
+ Path.of(source ?: error("Source can't be null"))
+ )
+ } else {
+ val validator = GithubPresubmitValidator()
+ validator.validateAll(
+ Path.of(sourceAll ?: error("SourceAll can't be null"))
+ )
+ }
+ } else {
+ val validator = InternalCIValidator(
+ agpVersion = agpVersion ?: error("agpVersion can't be null"),
+ repoLocation = repoLocation ?: error("repoLocation can't be null"),
+ gradlePath = gradlePath ?: error("gradlePath can't be null")
+ )
+ validator.validate(
+ sourceAll = Path.of(sourceAll ?: error("sourceAll can't be null")),
+ tmpFolder = if (tmpFolder != null) Path.of(tmpFolder) else null
+ )
+ }
+ }
+}
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/Converter.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/Converter.kt
new file mode 100644
index 0000000..fa8694c
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/Converter.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.android.gradle_recipe.converter.recipe.Recipe
+import java.nio.file.Path
+
+/** Interface for different converters.
+ * The objects are created and called from the RecipeConverter class,
+ * using a Template Method pattern.
+ */
+interface Converter {
+
+ /** Can converter convert this recipe
+ */
+ fun isConversionCompliant(recipe: Recipe): Boolean
+
+ /** sets the recipe to convert, before the conversion
+ */
+ fun setRecipe(recipe: Recipe) {}
+
+ /**
+ * converts settings.gradle
+ */
+ fun convertSettingsGradle(source: Path, target: Path)
+
+ /**
+ * converts settings.gradle.kts ==> same as settings.gradle
+ */
+ fun convertSettingsGradleKts(source: Path, target: Path) {
+ convertSettingsGradle(source, target)
+ }
+
+ /**
+ * converts build.gradle
+ */
+ fun convertBuildGradle(source: Path, target: Path)
+
+ /**
+ * converts build.gradle.kts ==> same as build.gradle
+ */
+ fun convertBuildGradleKts(source: Path, target: Path) {
+ convertBuildGradle(source, target)
+ }
+
+ /**
+ * converts gradle.wrapper
+ */
+ fun convertGradleWrapper(source: Path, target: Path)
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtils.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtils.kt
new file mode 100644
index 0000000..9a598d9
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtils.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+/** The functions in this file perform the conversion by adding and removing
+ * the placeholder data. The functions swap the placeholders
+ * ($AGP_VERSION, $AGP_REPOSITORY and $GRADLE_LOCATION) with actual
+ * values and commenting the relevant lines to use later
+ */
+
+/**
+ * for build.gradle[.kts] , settings.gradle[.kts] ==> wraps $AGP_VERSION or $AGP_REPOSITORY
+ */
+fun wrapGradlePlaceholders(originalLines: List<String>, from: String, to: String): List<String> {
+ return wrapPlaceholders(originalLines, from, to, "// ")
+}
+
+/**
+ * for build.gradle[.kts] , settings.gradle[.kts] ==> unwraps $AGP_VERSION or $AGP_REPOSITORY
+ */
+fun unwrapGradlePlaceholders(originalLines: List<String>): List<String> {
+ return unwrapPlaceholders(originalLines, "// ")
+}
+
+/**
+ * for gradle.wrapper.properties ==> wraps $GRADLE_LOCATION
+ */
+fun wrapGradleWrapperPlaceholders(originalLines: List<String>, from: String, to: String): List<String> {
+ return wrapPlaceholders(originalLines, from, to, "# ")
+}
+
+/**
+ * for gradle.wrapper.properties ==> unwraps $GRADLE_LOCATION
+ */
+fun unwrapGradleWrapperPlaceholders(originalLines: List<String>): List<String> {
+ return unwrapPlaceholders(originalLines, "# ")
+}
+
+/**
+ * replaces placeholders with actual values, without keeping the placeholders
+ */
+fun replacePlaceHolderWithValue(originalLines: List<String>, placeHolder: String, value: String): List<String> {
+ val result = buildList {
+ for (line in originalLines) {
+ if (line.contains(placeHolder)) {
+ if (value.isNotEmpty()) {
+ add(line.replace(placeHolder, value))
+ }
+
+ } else {
+ add(line)
+ }
+ }
+ }
+
+ return result
+}
+
+/**
+ * wraps both gradle and properties with commentOut
+ */
+private fun wrapPlaceholders(originalLines: List<String>, from: String, to: String, commentOut: String): List<String> {
+ val result = buildList {
+ for (line in originalLines) {
+ if (line.contains(from)) {
+ add("$commentOut>>> WORKING_COPY >>>")
+ add("$commentOut$line")
+
+ if (to.isNotEmpty()) {
+ val newLine: String = line.replace(from, to)
+ add(newLine)
+ }
+
+ add("$commentOut<<< WORKING_COPY <<<")
+ } else {
+ add(line)
+ }
+ }
+ }
+
+ return result
+}
+
+/**
+ * unwraps both gradle and properties with commentOut
+ */
+private fun unwrapPlaceholders(originalLines: List<String>, commentOut: String): List<String> {
+ var insideWorkBlock = false
+ val result = buildList {
+ for (line in originalLines) {
+ if (line == "$commentOut>>> WORKING_COPY >>>") {
+ insideWorkBlock = true
+ } else if (insideWorkBlock) {
+ if (line != "$commentOut<<< WORKING_COPY <<<") {
+ if (line.startsWith("$commentOut")) {
+ add(line.substring(commentOut.length))
+ }
+
+ } else {
+ // we are in "<<< WORKING_COPY <<<", means finished the block
+ insideWorkBlock = false
+ }
+ } else {
+ add(line)
+ }
+ }
+ }
+
+ return result
+}
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecipeConverter.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecipeConverter.kt
new file mode 100644
index 0000000..9fec3af
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecipeConverter.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.android.gradle_recipe.converter.recipe.Recipe
+import com.google.android.gradle_recipe.converter.recipe.RecipeMetadataParser
+import java.io.IOException
+import java.lang.System.err
+import java.nio.file.*
+import java.nio.file.attribute.BasicFileAttributes
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+
+val agpToGradleVersions = mapOf(
+ "3.6" to "5.6",
+ "4.0" to "6.1.1",
+ "4.1" to "6.5",
+ "4.2" to "6.7.1",
+ "7.0" to "7.0",
+ "7.1" to "7.2",
+ "7.2" to "7.3.3",
+ "7.4" to "7.3.3"
+)
+
+fun convertStringToMode(modeFromString: String?): RecipeConverter.Mode {
+ return if (modeFromString != null) {
+ RecipeConverter.Mode.valueOf(modeFromString.toString().uppercase())
+ } else {
+ RecipeConverter.Mode.RELEASE
+ }
+}
+
+data class ConversionResult(val recipe: Recipe, var isConversionSuccessful: Boolean)
+
+/**
+ * Converts the individual recipe, calculation the conversion mode by input parameters
+ */
+class RecipeConverter(
+ val agpVersion: String?,
+ repoLocation: String?,
+ gradleVersion: String?,
+ gradlePath: String?,
+ mode: Mode,
+ private val overwrite: Boolean,
+) {
+ private val converter: Converter
+
+ enum class Mode {
+ RELEASE, COPY, SOURCE
+ }
+
+ init {
+ converter = when (mode) {
+ Mode.COPY -> {
+ WorkingCopyConverter()
+ }
+
+ Mode.SOURCE -> {
+ SourceConverter()
+ }
+
+ Mode.RELEASE -> {
+ ReleaseConverter(
+ agpVersion = agpVersion ?: error("Must specify the AGP version for release"),
+ gradleVersion = gradleVersion,
+ repoLocation = repoLocation,
+ gradlePath = gradlePath
+ )
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ fun convert(source: Path, destination: Path): ConversionResult {
+ if (!source.isDirectory()) {
+ error("the source $source folder is not a directory")
+ }
+
+ if (destination.exists() && !isEmpty(destination)) {
+ if (!overwrite) {
+ error("the destination $destination folder is not empty, call converter with --overwrite to overwrite it")
+ } else {
+ destination.toFile().deleteRecursively()
+ }
+ }
+
+ val metadataParser = RecipeMetadataParser(source)
+ val recipe = Recipe(
+ minAgpVersion = metadataParser.minAgpVersion,
+ maxAgpVersion = metadataParser.maxAgpVersion,
+ tasks = metadataParser.tasks,
+ keywords = metadataParser.indexKeywords
+ )
+
+ val result = ConversionResult(recipe, false)
+
+ if (converter.isConversionCompliant(recipe)) {
+ converter.setRecipe(recipe)
+ result.isConversionSuccessful = true
+
+ Files.walkFileTree(source, object : SimpleFileVisitor<Path>() {
+ @Throws(IOException::class)
+ override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
+ Files.createDirectories(destination.resolve(source.relativize(dir)))
+ return FileVisitResult.CONTINUE
+ }
+
+ @Throws(IOException::class)
+ override fun visitFile(sourceFile: Path, attrs: BasicFileAttributes): FileVisitResult {
+ val fileName = sourceFile.fileName.toString()
+ val destinationFile = destination.resolve(source.relativize(sourceFile))
+
+ when (fileName) {
+ "build.gradle" -> {
+ converter.convertBuildGradle(sourceFile, destinationFile)
+ }
+ "build.gradle.kts" -> {
+ converter.convertBuildGradleKts(sourceFile, destinationFile)
+ }
+ "settings.gradle" -> {
+ converter.convertSettingsGradle(sourceFile, destinationFile)
+ }
+ "settings.gradle.kts" -> {
+ converter.convertSettingsGradleKts(sourceFile, destinationFile)
+ }
+ "gradle-wrapper.properties" -> {
+ converter.convertGradleWrapper(sourceFile, destinationFile)
+ }
+ else -> {
+ Files.copy(sourceFile, destinationFile, StandardCopyOption.REPLACE_EXISTING)
+ }
+ }
+
+ return FileVisitResult.CONTINUE
+ }
+ })
+ } else {
+ err.println("Couldn't convert $source due to AGP version compliance ")
+ }
+
+ return result
+ }
+
+ @Throws(IOException::class)
+ fun isEmpty(path: Path): Boolean {
+ if (Files.isDirectory(path)) {
+ Files.list(path).use { entries -> return !entries.findFirst().isPresent }
+ }
+ return false
+ }
+}
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecursiveConverter.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecursiveConverter.kt
new file mode 100644
index 0000000..87e60f4
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/RecursiveConverter.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter.Mode
+import com.google.android.gradle_recipe.converter.recipe.visitRecipes
+import java.io.File
+import java.io.IOException
+import java.nio.file.Path
+import java.util.*
+import kotlin.io.path.exists
+
+const val INDEX_METADATA_FILE = "README.md"
+
+/* Recursive converter converts recipes from source of truth mode
+ to release mode
+ */
+class RecursiveConverter(
+ private val agpVersion: String?,
+ private var repoLocation: String?,
+ var gradleVersion: String?,
+ var gradlePath: String?,
+ private var overwrite: Boolean,
+) {
+
+ private val keywordsToRecipePaths = mutableMapOf<String, MutableList<Path>>()
+
+ @Throws(IOException::class)
+ fun convertAllRecipes(sourceAll: Path, destination: Path) {
+ if (!sourceAll.exists()) {
+ error("the source $sourceAll folder doesn't exist")
+ }
+
+ val recipeConverter = RecipeConverter(
+ agpVersion = agpVersion,
+ repoLocation = repoLocation,
+ gradleVersion = gradleVersion,
+ gradlePath = gradlePath,
+ mode = Mode.RELEASE,
+ overwrite = overwrite
+ )
+
+ visitRecipes(sourceAll) { recipeFolder: Path ->
+ val recipeRelativeName = sourceAll.relativize(recipeFolder)
+ val currentRecipeDestination = destination.resolve(recipeRelativeName)
+ val conversionResult = recipeConverter.convert(
+ recipeFolder,
+ currentRecipeDestination
+ )
+
+ if (conversionResult.isConversionSuccessful) {
+ for (keyword in conversionResult.recipe.keywords) {
+ val list = keywordsToRecipePaths.computeIfAbsent(keyword) { mutableListOf() }
+ list.add(recipeRelativeName)
+ }
+ }
+ }
+
+ writeRecipesIndexFile(keywordsToRecipePaths.toSortedMap(), destination)
+ }
+
+ private fun writeRecipesIndexFile(
+ keywordsToRecipePaths: MutableMap<String, MutableList<Path>>,
+ destination: Path,
+ ) {
+ val builder = StringBuilder()
+ val commaDelimiter = ", "
+ builder.appendLine("# Recipes Index")
+
+ keywordsToRecipePaths.keys.forEach { indexKeyword ->
+ builder.appendLine("* $indexKeyword - ")
+ val joiner = StringJoiner(commaDelimiter)
+
+ keywordsToRecipePaths[indexKeyword]?.forEach { recipeRelativePath ->
+ val line =
+ "[$recipeRelativePath]($recipeRelativePath)"
+ joiner.add(line)
+ }
+
+ builder.appendLine(joiner.toString())
+ }
+
+ File(
+ destination.resolve(INDEX_METADATA_FILE).toUri()
+ ).writeText(builder.toString())
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ReleaseConverter.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ReleaseConverter.kt
new file mode 100644
index 0000000..c0d1b12
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/ReleaseConverter.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.android.gradle_recipe.converter.recipe.Recipe
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.writeLines
+
+/** This mode is for a recipe that has no placeholders.
+ * This is for releases ando/or tests
+ */
+class ReleaseConverter(
+ private val agpVersion: String,
+ gradleVersion: String?,
+ repoLocation: String?,
+ gradlePath: String?,
+) : Converter {
+
+ private var pathToGradle: String = ""
+ private var pathToRepo: String = ""
+
+ init {
+
+ if (gradleVersion != null) {
+ pathToGradle = "https\\://services.gradle.org/distributions/gradle-" +
+ "$gradleVersion-bin.zip"
+ } else {
+ pathToRepo = repoLocation ?: error("must specify path to repo")
+ pathToGradle = gradlePath ?: error("must specify path to Gradle")
+ }
+ }
+
+ override fun isConversionCompliant(recipe: Recipe): Boolean {
+ return recipe.isCompliantWithAgp(agpVersion)
+ }
+
+ override fun convertBuildGradle(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = replacePlaceHolderWithValue(
+ originalLines,
+ "\$AGP_VERSION",
+ "\"$agpVersion\""
+ )
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+
+ override fun convertSettingsGradle(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = replacePlaceHolderWithValue(
+ originalLines, "\$AGP_REPOSITORY", "$pathToRepo"
+ )
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+
+ override fun convertGradleWrapper(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = replacePlaceHolderWithValue(
+ originalLines, "\$GRADLE_LOCATION", "$pathToGradle"
+ )
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/SourceConverter.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/SourceConverter.kt
new file mode 100644
index 0000000..2eb1213
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/SourceConverter.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.android.gradle_recipe.converter.recipe.Recipe
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.writeLines
+
+/** This mode has the placeholders ($AGP_VERSION etc') and
+ * this is how we store the recipes in the dev branch
+ */
+class SourceConverter : Converter {
+ override fun isConversionCompliant(recipe: Recipe): Boolean {
+ return true
+ }
+
+ override fun convertSettingsGradle(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = unwrapGradlePlaceholders(originalLines)
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+
+ override fun convertBuildGradle(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = unwrapGradlePlaceholders(originalLines)
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+
+ override fun convertGradleWrapper(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = unwrapGradleWrapperPlaceholders(originalLines)
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/WorkingCopyConverter.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/WorkingCopyConverter.kt
new file mode 100644
index 0000000..5738afd
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/converters/WorkingCopyConverter.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.android.gradle_recipe.converter.recipe.Recipe
+import com.google.android.gradle_recipe.converter.recipe.getAgpVersionMajorMinorFrom
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.writeLines
+
+/**
+ * This is the working copy where the recipe has static values for $AGP_VERSION, etc...
+ * but markers to revert them back to placeholders.
+ */
+class WorkingCopyConverter : Converter {
+ private var recipe: Recipe? = null
+ override fun isConversionCompliant(recipe: Recipe): Boolean {
+ return true
+ }
+
+ override fun setRecipe(recipe: Recipe) {
+ this.recipe = recipe
+ }
+
+ override fun convertBuildGradle(source: Path, target: Path) {
+ val agpVersion = recipe?.minAgpVersion ?: error("min Agp version is badly specified in the metadata")
+
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = wrapGradlePlaceholders(
+ originalLines, "\$AGP_VERSION", "\"$agpVersion\""
+ )
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+
+ override fun convertSettingsGradle(source: Path, target: Path) {
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = wrapGradlePlaceholders(
+ originalLines, "\$AGP_REPOSITORY", ""
+ )
+
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+
+ override fun convertGradleWrapper(source: Path, target: Path) {
+ // building the line
+ // distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+ val agpVersion = recipe?.minAgpVersion ?: error("min Agp version is badly specified in the metadata")
+
+ val agpVersionMajorMinor = getAgpVersionMajorMinorFrom(agpVersion)
+ val gradleVersion = agpToGradleVersions[agpVersionMajorMinor]
+ ?: error("Can't deduce the gradle version from the recipe metadata")
+
+ val originalLines = Files.readAllLines(source)
+ val resultLines: List<String> = wrapGradleWrapperPlaceholders(
+ originalLines,
+ "\$GRADLE_LOCATION",
+ "https\\://services.gradle.org/distributions/gradle-$gradleVersion-bin.zip"
+ )
+ target.writeLines(resultLines, Charsets.UTF_8)
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/Recipe.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/Recipe.kt
new file mode 100644
index 0000000..d36e27f
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/Recipe.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.recipe
+
+import com.github.rising3.semver.SemVer
+import java.io.File
+import java.nio.file.Path
+
+/** A recipe metadata model
+ *
+ */
+class Recipe(
+ val minAgpVersion: String,
+ val maxAgpVersion: String?,
+ val tasks: List<String>,
+ val keywords: List<String>,
+) {
+
+ fun isCompliantWithAgp(agpVersion: String): Boolean {
+ val min = minAgpVersion
+ val max = maxAgpVersion
+
+ return if (max != null) {
+ SemVer.parse(agpVersion) >= SemVer.parse(min) && SemVer.parse(agpVersion) <= SemVer.parse(max)
+ } else {
+ // when maxAgpVersion is not specified
+ SemVer.parse(agpVersion) >= SemVer.parse(min)
+ }
+ }
+}
+
+fun getAgpVersionMajorMinorFrom(agpVersion: String): String {
+ return SemVer.parse(agpVersion).major.toString() + "." + SemVer.parse(agpVersion).minor.toString()
+}
+
+fun isRecipeFolder(folder: Path) = File(folder.toFile(), RECIPE_METADATA_FILE).exists()
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeMetadataParser.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeMetadataParser.kt
new file mode 100644
index 0000000..a80c897
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeMetadataParser.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.recipe
+
+import org.tomlj.Toml
+import org.tomlj.TomlParseResult
+import java.io.File
+import java.nio.file.Path
+
+const val RECIPE_METADATA_FILE = "recipe_metadata.toml"
+
+/** Recipe metadata parser, that reads and parses the metadata file
+ * from the recipe folder
+ */
+class RecipeMetadataParser(recipeFolder: Path) {
+ private var parseResult: TomlParseResult = Toml.parse(File(recipeFolder.toFile(), RECIPE_METADATA_FILE).toPath())
+ val minAgpVersion: String
+ val maxAgpVersion: String?
+ val tasks: List<String>
+ val indexKeywords: List<String>
+
+ init {
+ if (parseResult.hasErrors()) {
+ System.err.println("Recipes error/s at $recipeFolder")
+ parseResult.errors().forEach { error -> System.err.println(error.toString()) }
+ throw IllegalArgumentException("Badly formatted recipe_metadata.toml file")
+ }
+
+ val minAgpVersionFromMetadata = parseResult.getString("agpVersion.min")
+
+ if (minAgpVersionFromMetadata != null) {
+ minAgpVersion = minAgpVersionFromMetadata
+ } else {
+ throw IllegalArgumentException("the minimal AGP version must be specified in the recipe metadata file")
+ }
+
+ maxAgpVersion = parseResult.getString("agpVersion.max")
+
+ val tasksList = parseResult.getArray("gradleTasks.tasks")?.toList()
+ tasks = tasksList?.let { list ->
+ list.map {
+ it as String
+ }
+ } ?: emptyList()
+
+ val indexKeywordsList = parseResult.getArray("indexMetadata.index")?.toList()
+ indexKeywords = indexKeywordsList?.let { list ->
+ list.map {
+ it as String
+ }
+ } ?: emptyList()
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeUtils.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeUtils.kt
new file mode 100644
index 0000000..06038cc
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/recipe/RecipeUtils.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.recipe
+
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.exists
+import kotlin.io.path.isHidden
+
+/** Visitor function, recursively traverses rootFolder with recipes and
+ * activates recipeCallback for each found recipe.
+ */
+fun visitRecipes(rootFolder: Path, recipeCallback: (Path) -> Unit) {
+ if (!rootFolder.exists()) {
+ error("the source $rootFolder folder doesn't exist")
+ }
+
+ Files.walk(rootFolder).filter { currentPath: Path ->
+ Files.isDirectory(currentPath)
+ && currentPath != rootFolder
+ && !currentPath.isHidden()
+ && isRecipeFolder(currentPath)
+ }.forEach { recipeFolder: Path ->
+ recipeCallback(recipeFolder)
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GithubPresubmitValidator.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GithubPresubmitValidator.kt
new file mode 100644
index 0000000..28a702d
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GithubPresubmitValidator.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.validators
+
+import com.google.android.gradle_recipe.converter.recipe.visitRecipes
+import java.nio.file.Path
+
+/** This will test all recipes found against both they min and
+ * current/max version of AGP
+ */
+class GithubPresubmitValidator {
+
+ fun validateAll(sourceAll: Path) {
+ val recipeValidator = MinMaxCurrentAgpValidator(sourceAll)
+ visitRecipes(sourceAll) { recipeFolder: Path ->
+ recipeValidator.validate(recipeFolder)
+ }
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GradleTasksExecutor.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GradleTasksExecutor.kt
new file mode 100644
index 0000000..62d1b33
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/GradleTasksExecutor.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.validators
+
+import org.gradle.tooling.GradleConnector
+import org.gradle.tooling.ProjectConnection
+import java.lang.System.err
+import java.nio.file.Path
+
+/** Executes Gradle tasks via GradleConnector API
+ */
+class GradleTasksExecutor(projectDir: Path) {
+ private val connector: GradleConnector = GradleConnector.newConnector()
+
+ init {
+ connector.forProjectDirectory(projectDir.toFile())
+ }
+
+ fun executeTasks(tasks: List<String>) {
+ tasks.forEach { task ->
+ println("$task ")
+ executeTask(task)
+ }
+ }
+
+ private fun executeTask(vararg tasks: String?) {
+ try {
+ val connection: ProjectConnection = connector.connect()
+ val build: org.gradle.tooling.BuildLauncher = connection.newBuild()
+ build.forTasks(*tasks)
+ build.run()
+ connection.close()
+ } catch (e: RuntimeException) {
+ err.println("Task: ${tasks[0]} failed with, ${e.message}")
+ }
+ }
+}
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/InternalCIValidator.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/InternalCIValidator.kt
new file mode 100644
index 0000000..f9ae242
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/InternalCIValidator.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.validators
+
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter.Mode
+import com.google.android.gradle_recipe.converter.recipe.visitRecipes
+import java.io.IOException
+import java.nio.file.Path
+import kotlin.io.path.createTempDirectory
+
+/** Validates al the recipes against a very specific version
+ * of AGP, and Gradle, in specific locations
+ */
+class InternalCIValidator(
+ private val agpVersion: String,
+ private val repoLocation: String,
+ private val gradlePath: String,
+) {
+ @Throws(IOException::class)
+ fun validate(sourceAll: Path, tmpFolder: Path?) {
+
+ val converter = RecipeConverter(
+ agpVersion = agpVersion,
+ repoLocation = repoLocation,
+ gradleVersion = null,
+ gradlePath = gradlePath,
+ mode = Mode.RELEASE,
+ overwrite = true
+ )
+
+ visitRecipes(sourceAll) { recipeFolder: Path ->
+ val destinationFolder: Path
+
+ if (tmpFolder != null) {
+ destinationFolder = tmpFolder
+ } else {
+ destinationFolder = createTempDirectory()
+ destinationFolder.toFile().deleteOnExit()
+ }
+
+ val conversionResult = converter.convert(
+ source = recipeFolder, destination = destinationFolder
+ )
+
+ if (conversionResult.isConversionSuccessful) {
+ println("Validating: $destinationFolder with AGP: $agpVersion and Gradle: $gradlePath")
+ val tasksExecutor = GradleTasksExecutor(destinationFolder)
+ tasksExecutor.executeTasks(conversionResult.recipe.tasks)
+ }
+ }
+ }
+}
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/MinMaxCurrentAgpValidator.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/MinMaxCurrentAgpValidator.kt
new file mode 100644
index 0000000..dc86afa
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/MinMaxCurrentAgpValidator.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.validators
+
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter.Mode
+import com.google.android.gradle_recipe.converter.converters.agpToGradleVersions
+import com.google.android.gradle_recipe.converter.recipe.RecipeMetadataParser
+import com.google.android.gradle_recipe.converter.recipe.getAgpVersionMajorMinorFrom
+import java.nio.file.Path
+import kotlin.io.path.createTempDirectory
+
+/** Validates recipe from source mode and with currentAgpFileLocation, by calling validation
+ * with min and current/max AGP versions
+ */
+class MinMaxCurrentAgpValidator(private val currentAGPFileLocation: Path) {
+
+ private val currentAGPVersionFile = "currentAgpVersion.txt"
+
+ fun validate(source: Path) {
+ val recipeMetadataParser = RecipeMetadataParser(source)
+ val minAgpVersion = recipeMetadataParser.minAgpVersion
+ val maxAgpVersion = recipeMetadataParser.maxAgpVersion
+
+ validateRecipeFromSource(source, minAgpVersion)
+
+ if (maxAgpVersion != null) {
+ validateRecipeFromSource(source, maxAgpVersion)
+ } else {
+ var currentAgpVersion: String? = null
+ val currentAgpFile = currentAGPFileLocation.resolve(currentAGPVersionFile).toFile()
+
+ if (currentAgpFile.exists()) {
+ currentAgpVersion = currentAgpFile.readText()
+ }
+
+ if (currentAgpVersion != null) {
+ validateRecipeFromSource(source, currentAgpVersion)
+ } else {
+ error(
+ "Neither maxAgp version was defined in the metadata, " +
+ "nor currentAgp defined in $currentAGPVersionFile " +
+ "defined in $source ==> thus validated only with minAgp"
+ )
+ }
+ }
+ }
+
+ private fun validateRecipeFromSource(
+ from: Path,
+ agpVersion: String,
+ ) {
+ val gradleVersion = agpToGradleVersions[getAgpVersionMajorMinorFrom(agpVersion)]
+
+ val recipeConverter = RecipeConverter(
+ agpVersion = agpVersion,
+ gradleVersion = gradleVersion,
+ repoLocation = null,
+ gradlePath = null,
+ mode = Mode.RELEASE,
+ overwrite = true
+ )
+
+ val destinationFolder = createTempDirectory()
+ destinationFolder.toFile().deleteOnExit()
+
+ val conversionResult = recipeConverter.convert(
+ source = from, destination = destinationFolder
+ )
+
+ if (conversionResult.isConversionSuccessful) {
+ println("Validating: $destinationFolder with AGP: $agpVersion and Gradle: $gradleVersion")
+ val tasksExecutor = GradleTasksExecutor(destinationFolder)
+ tasksExecutor.executeTasks(conversionResult.recipe.tasks)
+ }
+ }
+} \ No newline at end of file
diff --git a/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/WorkingCopyValidator.kt b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/WorkingCopyValidator.kt
new file mode 100644
index 0000000..a916a47
--- /dev/null
+++ b/convert-tool/app/src/main/kotlin/com/google/android/gradle_recipe/converter/validators/WorkingCopyValidator.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.validators
+
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter
+import com.google.android.gradle_recipe.converter.converters.RecipeConverter.Mode
+import java.nio.file.Path
+import kotlin.io.path.createTempDirectory
+
+/** This will convert the recipe back to sources (using a temp folder),
+ * and then create 2 release versions, using the min and current/max versions of
+ * AGP and tests against these.
+ */
+class WorkingCopyValidator {
+
+ fun validate(recipeSource: Path) {
+ val recipeValidator = MinMaxCurrentAgpValidator(recipeSource)
+ recipeValidator.validate(convertToSourceOfTruth(recipeSource))
+ }
+
+ private fun convertToSourceOfTruth(from: Path): Path {
+ val sourceOfTruthTempDirectory: Path = createTempDirectory()
+ sourceOfTruthTempDirectory.toFile().deleteOnExit()
+
+ val convertToSourceTruth = RecipeConverter(
+ agpVersion = null,
+ gradleVersion = null,
+ repoLocation = null,
+ gradlePath = null,
+ mode = Mode.SOURCE,
+ overwrite = true
+ )
+ convertToSourceTruth.convert(
+ source = from, destination = sourceOfTruthTempDirectory
+ )
+
+ return sourceOfTruthTempDirectory
+ }
+}
diff --git a/convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/AGPVersionsTest.kt b/convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/AGPVersionsTest.kt
new file mode 100644
index 0000000..5b11302
--- /dev/null
+++ b/convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/AGPVersionsTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter
+
+import com.github.rising3.semver.SemVer
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+internal class AGPVersionsTest {
+ @Test
+ fun alphaBeta() =
+ assertThat(SemVer.parse("7.1.0-alpha") < SemVer.parse("7.1.0-beta5")).isTrue()
+
+ @Test
+ fun betaRc() = assertThat(SemVer.parse("7.1.0-beta") < SemVer.parse("7.1.0-rc5")).isTrue()
+
+ @Test
+ fun rc1Rc2() =
+ assertThat(SemVer.parse("7.1.0-rc1") < SemVer.parse("7.1.0-rc2")).isTrue()
+
+ @Test
+ fun rcFinal() = assertThat(SemVer.parse("7.1.0-rc1") < SemVer.parse("7.1.0")).isTrue()
+
+ @Test
+ fun finalFinal() = assertThat(SemVer.parse("7.1.0") < SemVer.parse("8.1.0")).isTrue()
+} \ No newline at end of file
diff --git a/convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtilsKtTest.kt b/convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtilsKtTest.kt
new file mode 100644
index 0000000..951238d
--- /dev/null
+++ b/convert-tool/app/src/test/kotlin/com/google/android/gradle_recipe/converter/converters/ConverterUtilsKtTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2022 Google, Inc.
+ *
+ * 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.android.gradle_recipe.converter.converters
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+internal class ConverterUtilsKtTest {
+
+ // build.gradle
+ private val buildGradleSource = listOf(
+ "id 'com.android.application' version \$AGP_VERSION apply false",
+ "id 'com.android.library' version \$AGP_VERSION apply false",
+ "id 'org.jetbrains.kotlin.android' version '1.6.10' apply false"
+ )
+
+ private val buildGradleWorkingCopy = listOf(
+ "// >>> WORKING_COPY >>>",
+ "// id 'com.android.application' version \$AGP_VERSION apply false",
+ "id 'com.android.application' version \"7.4.4\" apply false",
+ "// <<< WORKING_COPY <<<",
+ "// >>> WORKING_COPY >>>",
+ "// id 'com.android.library' version \$AGP_VERSION apply false",
+ "id 'com.android.library' version \"7.4.4\" apply false",
+ "// <<< WORKING_COPY <<<",
+ "id 'org.jetbrains.kotlin.android' version '1.6.10' apply false",
+ )
+
+ @Test
+ fun testBuildGradleRelease() {
+ val result = wrapGradlePlaceholders(buildGradleSource, "\$AGP_VERSION", "\"7.4.4\"")
+ assertThat(result).isEqualTo(buildGradleWorkingCopy)
+ }
+
+ @Test
+ fun testBuildGradleSource() {
+ val result = unwrapGradlePlaceholders(buildGradleWorkingCopy)
+ assertThat(result).isEqualTo(buildGradleSource)
+ }
+
+ // Tests for settings.gradle
+ private val settingsGradleSource = """
+pluginManagement {
+ repositories {
+${'$'}AGP_REPOSITORY
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+${'$'}AGP_REPOSITORY
+ google()
+ mavenCentral()
+ }
+}"""
+
+ private val settingsGradleTest = """
+pluginManagement {
+ repositories {
+// >>> WORKING_COPY >>>
+// ${'$'}AGP_REPOSITORY
+// <<< WORKING_COPY <<<
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+// >>> WORKING_COPY >>>
+// ${'$'}AGP_REPOSITORY
+// <<< WORKING_COPY <<<
+ google()
+ mavenCentral()
+ }
+}"""
+
+ private val settingsGradleRelease = """
+pluginManagement {
+ repositories {
+// >>> WORKING_COPY >>>
+// ${'$'}AGP_REPOSITORY
+..\..\private-repo\
+// <<< WORKING_COPY <<<
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+// >>> WORKING_COPY >>>
+// ${'$'}AGP_REPOSITORY
+..\..\private-repo\
+// <<< WORKING_COPY <<<
+ google()
+ mavenCentral()
+ }
+}"""
+
+ @Test
+ fun testSettingsGradleWorkingCopy() {
+ val result = wrapGradlePlaceholders(settingsGradleSource.lines(), "\$AGP_REPOSITORY", "")
+ assertThat(result).isEqualTo(settingsGradleTest.lines())
+ }
+
+ @Test
+ fun testSettingsGradleSource() {
+ val result = unwrapGradlePlaceholders(settingsGradleTest.lines())
+ assertThat(result).isEqualTo(settingsGradleSource.lines())
+ }
+
+ @Test
+ fun testSettingsGradleRelease() {
+ val result = wrapGradlePlaceholders(settingsGradleSource.lines(), "\$AGP_REPOSITORY", "..\\..\\private-repo\\")
+ assertThat(result).isEqualTo(settingsGradleRelease.lines())
+ }
+
+ // gradle wrapper tests
+ private val gradleWrapperSource = listOf(
+ "distributionUrl=\$GRADLE_LOCATION"
+ )
+
+ private val gradleWrapperRelease = listOf(
+ "# >>> WORKING_COPY >>>",
+ "# distributionUrl=\$GRADLE_LOCATION",
+ "distributionUrl=../../../tools/external/gradle/gradle-7.4-bin.zip",
+ "# <<< WORKING_COPY <<<",
+ )
+
+ @Test
+ fun testGradleWrapperRelease() {
+ val result =
+ wrapGradleWrapperPlaceholders(
+ gradleWrapperSource,
+ "\$GRADLE_LOCATION",
+ "../../../tools/external/gradle/gradle-7.4-bin.zip"
+ )
+ assertThat(result).isEqualTo(gradleWrapperRelease)
+ }
+
+ @Test
+ fun testGradleWrapperSource() {
+ val result = unwrapGradleWrapperPlaceholders(gradleWrapperRelease)
+ assertThat(gradleWrapperSource).isEqualTo(result)
+ }
+} \ No newline at end of file
diff --git a/convert-tool/gradle/libs.versions.toml b/convert-tool/gradle/libs.versions.toml
new file mode 100644
index 0000000..ca49019
--- /dev/null
+++ b/convert-tool/gradle/libs.versions.toml
@@ -0,0 +1,25 @@
+[versions]
+kotlin = "1.7.10"
+guava = "30.1.1-jre"
+kotlinx-cli = "0.3.4"
+tomlj = "1.0.0"
+semver = "0.3.1"
+gradle-api = "7.3-20210825160000+0000"
+slf4j = "1.7.10"
+truth = "1.1.3"
+
+[libraries]
+kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
+guava = { module = "com.google.guava:guava", version.ref = "guava" }
+kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinx-cli" }
+tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }
+semver = { module = "com.github.rising3:semver", version.ref = "semver" }
+gradle-api = { module = "org.gradle:gradle-tooling-api", version.ref = "gradle-api" }
+slf4j = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
+google-truth = { module = "com.google.truth:truth", version.ref = "truth" }
+
+[plugins]
+kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file
diff --git a/convert-tool/gradle/wrapper/gradle-wrapper.jar b/convert-tool/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7454180
--- /dev/null
+++ b/convert-tool/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/convert-tool/gradle/wrapper/gradle-wrapper.properties b/convert-tool/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..aa991fc
--- /dev/null
+++ b/convert-tool/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/convert-tool/gradlew b/convert-tool/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/convert-tool/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/convert-tool/gradlew.bat b/convert-tool/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/convert-tool/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/convert-tool/settings.gradle.kts b/convert-tool/settings.gradle.kts
new file mode 100644
index 0000000..07d09cb
--- /dev/null
+++ b/convert-tool/settings.gradle.kts
@@ -0,0 +1,9 @@
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ mavenCentral()
+ }
+}
+
+rootProject.name = "convert-tool"
+include("app") \ No newline at end of file