Skip to content

Commit

Permalink
Allow for separate recipe name and location
Browse files Browse the repository at this point in the history
Replaced the previously unused 'title' entry in the toml file with
'indexName' that is used as display name in the index. Also added
'destinationFolder'.

The goal is to allow us to have the "same" recipe name, and link,
while having a different source folder. This will be useful once we
start forking recipe to have different versions compatible with
different major versions of AGP.

This also opens the door with having display names in the index
be different from the folder name, since camel case isn't super
pretty.

Bug: N/A
Test: N/A
Change-Id: I73388385d21ab2f030c101fa30902e69d675be84
  • Loading branch information
ducrohet committed Dec 27, 2023
1 parent 6605b06 commit 8d54ba8
Show file tree
Hide file tree
Showing 29 changed files with 207 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import com.google.android.gradle_recipe.converter.converters.RecursiveConverter
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 java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.io.path.isDirectory
import kotlin.io.path.name
import kotlin.system.exitProcess
import kotlinx.cli.ArgParser
Expand Down Expand Up @@ -75,36 +79,43 @@ fun main(args: Array<String>) {
val finalSource = source
val finalSourceAll = sourceAll

if (finalSource != null) {
// compute a better destination. This is based on the last segment of the source folder.
val sourcePath = Path.of(finalSource)
val destPath = Path.of(
destination ?: printErrorAndTerminate("destination must be specified"))
.resolve(sourcePath.name)
val destinationPath: Path =
Path.of(destination ?: printErrorAndTerminate("destination must be specified"))

if (!destinationPath.isDirectory()) {
printErrorAndTerminate("Folder does not exist: ${destinationPath.toAbsolutePath()}")
}

if (!destinationPath.isEmptyExceptForHidden()) {
if (!overwrite) {
error("the destination $destinationPath folder is not empty, call converter with --overwrite to overwrite it")
} else {
destinationPath.deleteNonHiddenRecursively()
}
}

if (finalSource != null) {
RecipeConverter(
agpVersion = agpVersion,
repoLocation = repoLocation,
gradleVersion = gradleVersion,
gradlePath = gradlePath,
mode = mode ?: RELEASE,
overwrite = overwrite,
branchRoot = branchRoot,
).convert(
source = sourcePath,
destination = destPath
source = Path.of(finalSource),
destination = destinationPath
)
} else if (finalSourceAll != null) {
RecursiveConverter(
agpVersion = agpVersion,
repoLocation = repoLocation,
gradleVersion = gradleVersion,
gradlePath = gradlePath,
overwrite = overwrite,
branchRoot = branchRoot,
).convertAllRecipes(
sourceAll = Path.of(finalSourceAll),
destination = Path.of(destination ?: printErrorAndTerminate("destination must be specified"))
destination = destinationPath
)
} else {
printErrorAndTerminate("one of source or sourceAll must be specified")
Expand Down Expand Up @@ -225,4 +236,15 @@ private fun validateNullArg(arg: Any?, msg: String) {
private fun printErrorAndTerminate(msg: String): Nothing {
System.err.println(msg)
exitProcess(1)
}
}

private fun Path.isEmptyExceptForHidden(): Boolean = !Files.list(this).anyMatch { !it.name.startsWith('.') }

@OptIn(ExperimentalPathApi::class)
private fun Path.deleteNonHiddenRecursively() {
Files.list(this).filter {
!it.name.startsWith('.')
}.forEach {
it.deleteRecursively()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import java.nio.file.attribute.BasicFileAttributes
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.name
import kotlin.io.path.readLines

private const val VERSION_MAPPING = "version_mappings.txt"
Expand Down Expand Up @@ -99,7 +100,6 @@ class RecipeConverter(
gradleVersion: String?,
gradlePath: String?,
private val mode: Mode,
private val overwrite: Boolean,
branchRoot: Path,
private val generateWrapper: Boolean = true,
) {
Expand Down Expand Up @@ -151,40 +151,38 @@ class RecipeConverter(
}
}

@Throws(IOException::class)
/**
* Converts a recipe from [source] into [destination]
*
* @param source the source folder containing the recipe.
* @param destination the destination folder. A new folder will be created inside to contain the recipe
*
*/
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 recipeData = RecipeData.loadFrom(source, mode)

val recipeData = RecipeData.loadFrom(source)
val recipeDestination = destination.resolve(recipeData.destinationFolder)

val success = if (converter.isConversionCompliant(recipeData)) {
converter.recipeData = recipeData

Files.walkFileTree(source, object : SimpleFileVisitor<Path>() {
@Throws(IOException::class)
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
if (accept(dir.toFile())) {
Files.createDirectories(destination.resolve(source.relativize(dir)))
Files.createDirectories(recipeDestination.resolve(source.relativize(dir)))
return FileVisitResult.CONTINUE
}

return FileVisitResult.SKIP_SUBTREE
}

@Throws(IOException::class)
override fun visitFile(sourceFile: Path, attrs: BasicFileAttributes): FileVisitResult {
val fileName = sourceFile.fileName.toString()
val destinationFile = destination.resolve(source.relativize(sourceFile))
val destinationFile = recipeDestination.resolve(source.relativize(sourceFile))

when (fileName) {
"build.gradle" -> {
Expand Down Expand Up @@ -223,7 +221,7 @@ class RecipeConverter(
})

if (generateWrapper && mode != Mode.SOURCE) {
converter.copyGradleFolder(destination)
converter.copyGradleFolder(recipeDestination)
}

true
Expand All @@ -234,12 +232,4 @@ class RecipeConverter(

return ConversionResult(recipeData, success)
}

@Throws(IOException::class)
fun isEmpty(path: Path): Boolean {
if (Files.isDirectory(path)) {
Files.list(path).use { entries -> return !entries.findFirst().isPresent }
}
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ class RecursiveConverter(
private var repoLocation: String?,
var gradleVersion: String?,
var gradlePath: String?,
private val overwrite: Boolean,
private val branchRoot: Path,
) {

private val keywordsToRecipePaths = mutableMapOf<String, MutableList<Path>>()
private val keywordsToRecipePaths = mutableMapOf<String, MutableList<IndexData>>()

private data class IndexData(
val title: String,
val link: String
)

@Throws(IOException::class)
fun convertAllRecipes(sourceAll: Path, destination: Path) {
Expand All @@ -53,22 +57,21 @@ class RecursiveConverter(
gradleVersion = gradleVersion,
gradlePath = gradlePath,
mode = Mode.RELEASE,
overwrite = overwrite,
branchRoot = branchRoot,
)

visitRecipes(sourceAll) { recipeFolder: Path ->
val recipeRelativeName = sourceAll.relativize(recipeFolder)
val currentRecipeDestination = destination.resolve(recipeRelativeName)
val conversionResult = recipeConverter.convert(
recipeFolder,
currentRecipeDestination
)
val conversionResult = recipeConverter.convert(recipeFolder, destination)

if (conversionResult.isConversionSuccessful) {
for (keyword in conversionResult.recipeData.keywords) {
val list = keywordsToRecipePaths.computeIfAbsent(keyword) { mutableListOf() }
list.add(recipeRelativeName)
list.add(
IndexData(
conversionResult.recipeData.indexName,
conversionResult.recipeData.destinationFolder
)
)
}
}
}
Expand All @@ -78,7 +81,7 @@ class RecursiveConverter(
}

private fun writeRecipesIndexFile(
keywordsToRecipePaths: MutableMap<String, MutableList<Path>>,
keywordsToRecipePaths: MutableMap<String, MutableList<IndexData>>,
destination: Path,
agpVersion: String,
) {
Expand All @@ -98,10 +101,8 @@ class RecursiveConverter(
builder.appendLine("* $indexKeyword - ")
val joiner = StringJoiner(commaDelimiter)

keywordsToRecipePaths[indexKeyword]?.forEach { recipeRelativePath ->
val line =
"[$recipeRelativePath]($recipeRelativePath)"
joiner.add(line)
keywordsToRecipePaths[indexKeyword]?.forEach { data ->
joiner.add("[${data.title}](${data.link})")
}

builder.appendLine(joiner.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@
package com.google.android.gradle_recipe.converter.recipe

import com.github.rising3.semver.SemVer
import com.google.android.gradle_recipe.converter.converters.RecipeConverter
import org.tomlj.Toml
import org.tomlj.TomlParseResult
import java.io.File
import java.nio.file.Path
import kotlin.io.path.name

private const val RECIPE_METADATA_FILE = "recipe_metadata.toml"
const val RECIPE_METADATA_FILE = "recipe_metadata.toml"

/**
* Recipe Data representing the content of `recipe_metadata.toml`
*/
class RecipeData private constructor(
val title: String,
/** The name of the recipe to show in the index */
val indexName: String,
/** the name of the folder that should contain the recipe */
val destinationFolder: String,
val minAgpVersion: String,
val maxAgpVersion: String?,
val tasks: List<String>,
Expand All @@ -47,7 +52,7 @@ class RecipeData private constructor(
}

companion object {
fun loadFrom(recipeFolder: Path): RecipeData {
fun loadFrom(recipeFolder: Path, mode: RecipeConverter.Mode): RecipeData {
val toml = recipeFolder.resolve(RECIPE_METADATA_FILE)
val parseResult: TomlParseResult = Toml.parse(toml)

Expand All @@ -57,8 +62,35 @@ class RecipeData private constructor(
throw IllegalArgumentException("Unable to read $toml")
}

val indexName = if (mode == RecipeConverter.Mode.RELEASE) {
val entry = parseResult.getString("indexName")
if (entry.isNullOrBlank()) {
recipeFolder.name
} else {
entry
}
} else {
recipeFolder.name
}

val destinationFolder = if (mode == RecipeConverter.Mode.RELEASE) {
val entry = parseResult.getString("destinationFolder")
if (entry.isNullOrBlank()) {
recipeFolder.name
} else {
// check there's no path separator in there
if (entry.contains('/')) {
error("destinationFolder value ('$entry') cannot contain / character ($recipeFolder)")
}
entry
}
} else {
recipeFolder.name
}

return RecipeData(
title = parseResult.getString("title") ?: error("Did not find mandatory 'title` entry in $toml"),
indexName = indexName,
destinationFolder = destinationFolder,
minAgpVersion = parseResult.getString("agpVersion.min")
?: error("Did not find mandatory 'agpVersion.min' in $toml"),
maxAgpVersion = parseResult.getString("agpVersion.max"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,14 @@ class InternalCIValidator(
gradleVersion = null,
gradlePath = gradlePath,
mode = Mode.RELEASE,
overwrite = true,
branchRoot = branchRoot,
)

visitRecipes(sourceAll) { recipeFolder: Path ->
val destinationFolder: Path
val destinationFolder = tmpFolder ?: createTempDirectory().also {
it.toFile().deleteOnExit()
}

if (tmpFolder != null) {
destinationFolder = tmpFolder
} else {
destinationFolder = createTempDirectory()
destinationFolder.toFile().deleteOnExit()
}
visitRecipes(sourceAll) { recipeFolder: Path ->

val conversionResult = converter.convert(
source = recipeFolder, destination = destinationFolder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MinMaxCurrentAgpValidator(

fun validate(recipeFolder: Path, name: String? = null) {
val finalName = name ?: recipeFolder.name
val recipeDataMetadataParser = RecipeData.loadFrom(recipeFolder)
val recipeDataMetadataParser = RecipeData.loadFrom(recipeFolder, Mode.RELEASE)

validateRecipeFromSource(finalName, recipeFolder, recipeDataMetadataParser.minAgpVersion)
validateRecipeFromSource(finalName, recipeFolder, recipeDataMetadataParser.maxAgpVersion ?: maxAgp)
Expand All @@ -58,12 +58,10 @@ class MinMaxCurrentAgpValidator(
repoLocation = null,
gradlePath = null,
mode = Mode.RELEASE,
overwrite = true,
branchRoot = branchRoot,
)

val destinationFolder = createTempDirectory()
destinationFolder.toFile().deleteOnExit()
val destinationFolder = createTempDirectory().also { it.toFile().deleteOnExit() }

val conversionResult = recipeConverter.convert(
source = from, destination = destinationFolder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,18 @@ class WorkingCopyValidator(
}

private fun convertToSourceOfTruth(from: Path): Path {
val sourceOfTruthTempDirectory: Path = createTempDirectory()
sourceOfTruthTempDirectory.toFile().deleteOnExit()
val destination: Path = createTempDirectory().also { it.toFile().deleteOnExit() }

val convertToSourceTruth = RecipeConverter(
agpVersion = null,
gradleVersion = null,
repoLocation = null,
gradlePath = null,
mode = Mode.SOURCE,
overwrite = true,
branchRoot = branchRoot,
)
convertToSourceTruth.convert(
source = from, destination = sourceOfTruthTempDirectory
)
convertToSourceTruth.convert(source = from, destination = destination)

return sourceOfTruthTempDirectory
return destination
}
}
5 changes: 4 additions & 1 deletion recipes/addBuildTypeUsingDslFinalize/recipe_metadata.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
title = "Add a BuildType using dsl finalization block"
# optional (if present and non-blank) name to use in the index
indexName = ""
# optional (if present and non-blank) folder name to use when converting recipe in RELEASE mode
destinationFolder = ""

description ="""
This recipe will use the `finalizeDsl` block to add a build type programmatically.
Expand Down
Loading

0 comments on commit 8d54ba8

Please sign in to comment.