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 login = providers.environmentVariable("CROWDIN_LOGIN")
val projectName = extension.projectName val key = providers.environmentVariable("CROWDIN_PROJECT_KEY")
if (projectName.isEmpty()) { val buildOnApi =
throw GradleException(EXCEPTION_MESSAGE) if (login.isPresent && key.isPresent) {
tasks.register<BuildOnApiTask>("buildOnApi") {
crowdinIdentifier.set(extension.crowdinIdentifier)
crowdinLogin.set(login)
crowdinKey.set(key)
}
} else {
null
} }
val buildOnApi = val downloadCrowdin =
tasks.register("buildOnApi") { tasks.register<Download>("downloadCrowdin") {
doLast { if (buildOnApi != null) dependsOn(buildOnApi)
val login = providers.environmentVariable("CROWDIN_LOGIN") src(
val key = providers.environmentVariable("CROWDIN_PROJECT_KEY") "https://crowdin.com/backend/download/project/${extension.crowdinIdentifier.get()}.zip"
if (!login.isPresent) { )
throw GradleException("CROWDIN_LOGIN environment variable must be set") dest(layout.buildDirectory.file("translations.zip"))
} overwrite(true)
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 downloadCrowdin =
tasks.register<Download>("downloadCrowdin") {
dependsOn(buildOnApi)
src("https://crowdin.com/backend/download/project/$projectName.zip")
dest("$buildDirectory/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")
}
val extractStrings =
tasks.register<Copy>("extractStrings") {
dependsOn(extractCrowdin)
from("$buildDirectory/translations/")
into("${projectDir}/src/")
setFinalizedBy(setOf("removeIncompleteStrings"))
}
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()
}
}
}
valuesDirectories.forEach { dir ->
if (dir.listFiles().isNullOrEmpty()) {
dir.delete()
}
}
}
}
} }
tasks.register("crowdin") { val extractCrowdin =
dependsOn(extractStrings) tasks.register<Copy>("extractCrowdin") {
if (!extension.skipCleanup) { from(zipTree(downloadCrowdin.map { it.outputFiles.first() }))
doLast { into(layout.buildDirectory.dir("translations"))
File("$buildDirectory/translations").deleteRecursively()
File("$buildDirectory/nonFree-translations").deleteRecursively()
File("$buildDirectory/translations.zip").delete()
}
}
} }
val extractStrings =
tasks.register<Copy>("extractStrings") {
from(extractCrowdin.map { it.destinationDir })
into(layout.projectDirectory.dir("src"))
}
val removeIncompleteStrings =
tasks.register<StringCleanupTask>("removeIncompleteStrings") {
sourceDirectory.set(
objects.directoryProperty().fileProvider(extractStrings.map { it.destinationDir })
)
}
tasks.register<Delete>("crowdin") {
dependsOn(removeIncompleteStrings)
delete =
if (extension.skipCleanup.getOrElse(false)) {
emptySet()
} else {
setOf(
extractStrings.map { it.source },
downloadCrowdin.map { it.outputFiles },
)
}
} }
} }
} }
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
}
} }