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 { crowdin {
projectName = "android-password-store" crowdinIdentifier = "android-password-store"
skipCleanup = false skipCleanup = false
} }

View file

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

View file

@ -6,18 +6,12 @@
package app.passwordstore.gradle.crowdin package app.passwordstore.gradle.crowdin
import de.undercouch.gradle.tasks.download.Download 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.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.Copy import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Delete
import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.register
import org.w3c.dom.Document
private const val EXCEPTION_MESSAGE = private const val EXCEPTION_MESSAGE =
"""Applying `crowdin-plugin` requires a projectName to be configured via the "crowdin" extension.""" """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) { override fun apply(project: Project) {
with(project) { with(project) {
val buildDirectory = layout.buildDirectory.asFile.get()
val extension = extensions.create<CrowdinExtension>("crowdin") 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 login = providers.environmentVariable("CROWDIN_LOGIN")
val key = providers.environmentVariable("CROWDIN_PROJECT_KEY") val key = providers.environmentVariable("CROWDIN_PROJECT_KEY")
if (!login.isPresent) { val buildOnApi =
throw GradleException("CROWDIN_LOGIN environment variable must be set") if (login.isPresent && key.isPresent) {
} tasks.register<BuildOnApiTask>("buildOnApi") {
if (!key.isPresent) { crowdinIdentifier.set(extension.crowdinIdentifier)
throw GradleException("CROWDIN_PROJECT_KEY environment variable must be set") crowdinLogin.set(login)
} crowdinKey.set(key)
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()
} }
} else {
null
} }
val downloadCrowdin = val downloadCrowdin =
tasks.register<Download>("downloadCrowdin") { tasks.register<Download>("downloadCrowdin") {
dependsOn(buildOnApi) if (buildOnApi != null) dependsOn(buildOnApi)
src("https://crowdin.com/backend/download/project/$projectName.zip") src(
dest("$buildDirectory/translations.zip") "https://crowdin.com/backend/download/project/${extension.crowdinIdentifier.get()}.zip"
)
dest(layout.buildDirectory.file("translations.zip"))
overwrite(true) overwrite(true)
} }
val extractCrowdin = val extractCrowdin =
tasks.register<Copy>("extractCrowdin") { tasks.register<Copy>("extractCrowdin") {
dependsOn(downloadCrowdin) from(zipTree(downloadCrowdin.map { it.outputFiles.first() }))
doFirst { File(buildDir, "translations").deleteRecursively() } into(layout.buildDirectory.dir("translations"))
from(zipTree("$buildDirectory/translations.zip"))
into("$buildDirectory/translations")
} }
val extractStrings = val extractStrings =
tasks.register<Copy>("extractStrings") { tasks.register<Copy>("extractStrings") {
dependsOn(extractCrowdin) from(extractCrowdin.map { it.destinationDir })
from("$buildDirectory/translations/") into(layout.projectDirectory.dir("src"))
into("${projectDir}/src/")
setFinalizedBy(setOf("removeIncompleteStrings"))
} }
tasks.register("removeIncompleteStrings") { val removeIncompleteStrings =
doLast { tasks.register<StringCleanupTask>("removeIncompleteStrings") {
val sourceSets = arrayOf("main", "nonFree") sourceDirectory.set(
for (sourceSet in sourceSets) { objects.directoryProperty().fileProvider(extractStrings.map { it.destinationDir })
val fileTreeWalk = projectDir.resolve("src/$sourceSet").walkTopDown() )
val valuesDirectories = }
fileTreeWalk.filter { it.isDirectory }.filter { it.name.startsWith("values") } tasks.register<Delete>("crowdin") {
val stringFiles = fileTreeWalk.filter { it.name == "strings.xml" } dependsOn(removeIncompleteStrings)
val sourceFile = delete =
stringFiles.firstOrNull { it.path.endsWith("values/strings.xml") } if (extension.skipCleanup.getOrElse(false)) {
?: throw GradleException("No root strings.xml found in '$sourceSet' sourceSet") emptySet()
val sourceDoc = parseDocument(sourceFile) } else {
val baselineStringCount = countStrings(sourceDoc) setOf(
val threshold = 0.80 * baselineStringCount extractStrings.map { it.source },
stringFiles.forEach { file -> downloadCrowdin.map { it.outputFiles },
if (file != sourceFile) { )
val doc = parseDocument(file)
val stringCount = countStrings(doc)
if (stringCount < threshold) {
file.delete()
} }
} }
} }
valuesDirectories.forEach { dir ->
if (dir.listFiles().isNullOrEmpty()) {
dir.delete()
}
}
}
}
}
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
} }
} }