refactor(build): wire up CC-compatible task dependencies for Crowdin

This commit is contained in:
Harsh Shandilya 2023-03-21 13:28:56 +05:30
parent e8bd4c9bc0
commit db7756638a
No known key found for this signature in database
3 changed files with 51 additions and 111 deletions

View file

@ -16,7 +16,7 @@ plugins {
}
crowdin {
projectName = "android-password-store"
crowdinIdentifier = "android-password-store"
skipCleanup = false
}

View file

@ -5,16 +5,18 @@
package app.passwordstore.gradle.crowdin
import org.gradle.api.provider.Property
/** Extension for configuring [CrowdinDownloadPlugin] */
interface CrowdinExtension {
/** Configure the project name on Crowdin */
var projectName: String
val crowdinIdentifier: Property<String>
/**
* Don't delete downloaded and extracted translation archives from build directory.
*
* Useful for debugging.
*/
var skipCleanup: Boolean
val skipCleanup: Property<Boolean>
}

View file

@ -6,18 +6,12 @@
package app.passwordstore.gradle.crowdin
import de.undercouch.gradle.tasks.download.Download
import java.io.File
import java.util.concurrent.TimeUnit
import javax.xml.parsers.DocumentBuilderFactory
import okhttp3.OkHttpClient
import okhttp3.Request
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Delete
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.register
import org.w3c.dom.Document
private const val EXCEPTION_MESSAGE =
"""Applying `crowdin-plugin` requires a projectName to be configured via the "crowdin" extension."""
@ -29,112 +23,56 @@ class CrowdinDownloadPlugin : Plugin<Project> {
override fun apply(project: Project) {
with(project) {
val buildDirectory = layout.buildDirectory.asFile.get()
val extension = extensions.create<CrowdinExtension>("crowdin")
afterEvaluate {
val projectName = extension.projectName
if (projectName.isEmpty()) {
throw GradleException(EXCEPTION_MESSAGE)
}
val buildOnApi =
tasks.register("buildOnApi") {
doLast {
val login = providers.environmentVariable("CROWDIN_LOGIN")
val key = providers.environmentVariable("CROWDIN_PROJECT_KEY")
if (!login.isPresent) {
throw GradleException("CROWDIN_LOGIN environment variable must be set")
}
if (!key.isPresent) {
throw GradleException("CROWDIN_PROJECT_KEY environment variable must be set")
}
val client =
OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
.readTimeout(5, TimeUnit.MINUTES)
.callTimeout(10, TimeUnit.MINUTES)
.build()
val url = CROWDIN_BUILD_API_URL.format(projectName, login.get(), key.get())
val request = Request.Builder().url(url).get().build()
client.newCall(request).execute().close()
val buildOnApi =
if (login.isPresent && key.isPresent) {
tasks.register<BuildOnApiTask>("buildOnApi") {
crowdinIdentifier.set(extension.crowdinIdentifier)
crowdinLogin.set(login)
crowdinKey.set(key)
}
} else {
null
}
val downloadCrowdin =
tasks.register<Download>("downloadCrowdin") {
dependsOn(buildOnApi)
src("https://crowdin.com/backend/download/project/$projectName.zip")
dest("$buildDirectory/translations.zip")
if (buildOnApi != null) dependsOn(buildOnApi)
src(
"https://crowdin.com/backend/download/project/${extension.crowdinIdentifier.get()}.zip"
)
dest(layout.buildDirectory.file("translations.zip"))
overwrite(true)
}
val extractCrowdin =
tasks.register<Copy>("extractCrowdin") {
dependsOn(downloadCrowdin)
doFirst { File(buildDir, "translations").deleteRecursively() }
from(zipTree("$buildDirectory/translations.zip"))
into("$buildDirectory/translations")
from(zipTree(downloadCrowdin.map { it.outputFiles.first() }))
into(layout.buildDirectory.dir("translations"))
}
val extractStrings =
tasks.register<Copy>("extractStrings") {
dependsOn(extractCrowdin)
from("$buildDirectory/translations/")
into("${projectDir}/src/")
setFinalizedBy(setOf("removeIncompleteStrings"))
from(extractCrowdin.map { it.destinationDir })
into(layout.projectDirectory.dir("src"))
}
tasks.register("removeIncompleteStrings") {
doLast {
val sourceSets = arrayOf("main", "nonFree")
for (sourceSet in sourceSets) {
val fileTreeWalk = projectDir.resolve("src/$sourceSet").walkTopDown()
val valuesDirectories =
fileTreeWalk.filter { it.isDirectory }.filter { it.name.startsWith("values") }
val stringFiles = fileTreeWalk.filter { it.name == "strings.xml" }
val sourceFile =
stringFiles.firstOrNull { it.path.endsWith("values/strings.xml") }
?: throw GradleException("No root strings.xml found in '$sourceSet' sourceSet")
val sourceDoc = parseDocument(sourceFile)
val baselineStringCount = countStrings(sourceDoc)
val threshold = 0.80 * baselineStringCount
stringFiles.forEach { file ->
if (file != sourceFile) {
val doc = parseDocument(file)
val stringCount = countStrings(doc)
if (stringCount < threshold) {
file.delete()
val removeIncompleteStrings =
tasks.register<StringCleanupTask>("removeIncompleteStrings") {
sourceDirectory.set(
objects.directoryProperty().fileProvider(extractStrings.map { it.destinationDir })
)
}
}
}
valuesDirectories.forEach { dir ->
if (dir.listFiles().isNullOrEmpty()) {
dir.delete()
tasks.register<Delete>("crowdin") {
dependsOn(removeIncompleteStrings)
delete =
if (extension.skipCleanup.getOrElse(false)) {
emptySet()
} else {
setOf(
extractStrings.map { it.source },
downloadCrowdin.map { it.outputFiles },
)
}
}
}
}
}
tasks.register("crowdin") {
dependsOn(extractStrings)
if (!extension.skipCleanup) {
doLast {
File("$buildDirectory/translations").deleteRecursively()
File("$buildDirectory/nonFree-translations").deleteRecursively()
File("$buildDirectory/translations.zip").delete()
}
}
}
}
}
}
private fun parseDocument(file: File): Document {
val dbFactory = DocumentBuilderFactory.newInstance()
val documentBuilder = dbFactory.newDocumentBuilder()
return documentBuilder.parse(file)
}
private fun countStrings(document: Document): Int {
// Normalization is beneficial for us
// https://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work
document.documentElement.normalize()
return document.getElementsByTagName("string").length
}
}