Source added
This commit is contained in:
parent
b2864b500e
commit
ba28ca859e
8352 changed files with 1487182 additions and 1 deletions
13
build-logic/build.gradle.kts
Normal file
13
build-logic/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
plugins {
|
||||
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply(from = "${rootDir}/../constants.gradle.kts")
|
||||
|
||||
44
build-logic/plugins/build.gradle.kts
Normal file
44
build-logic/plugins/build.gradle.kts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import org.gradle.kotlin.dsl.extra
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
alias(libs.plugins.ktlint)
|
||||
id("groovy-gradle-plugin")
|
||||
}
|
||||
|
||||
val signalJavaVersion: JavaVersion by rootProject.extra
|
||||
val signalKotlinJvmTarget: String by rootProject.extra
|
||||
|
||||
java {
|
||||
sourceCompatibility = signalJavaVersion
|
||||
targetCompatibility = signalJavaVersion
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(signalKotlinJvmTarget))
|
||||
}
|
||||
compilerOptions {
|
||||
suppressWarnings = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.gradle.plugin)
|
||||
implementation(libs.android.library)
|
||||
implementation(libs.android.application)
|
||||
implementation(libs.ktlint)
|
||||
implementation(project(":tools"))
|
||||
|
||||
// These allow us to reference the dependency catalog inside of our compiled plugins
|
||||
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
implementation(files(testLibs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
}
|
||||
|
||||
ktlint {
|
||||
filter {
|
||||
exclude { element ->
|
||||
element.file.path.contains("/build/generated-sources")
|
||||
}
|
||||
}
|
||||
}
|
||||
2300
build-logic/plugins/src/main/java/Licenses.kt
Normal file
2300
build-logic/plugins/src/main/java/Licenses.kt
Normal file
File diff suppressed because it is too large
Load diff
7
build-logic/plugins/src/main/java/ktlint.gradle.kts
Normal file
7
build-logic/plugins/src/main/java/ktlint.gradle.kts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("org.jlleitschuh.gradle.ktlint")
|
||||
}
|
||||
|
||||
ktlint {
|
||||
version.set("1.2.1")
|
||||
}
|
||||
223
build-logic/plugins/src/main/java/licenses.gradle.kts
Normal file
223
build-logic/plugins/src/main/java/licenses.gradle.kts
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import Licenses.LicenseData
|
||||
import groovy.xml.XmlSlurper
|
||||
import groovy.xml.slurpersupport.GPathResult
|
||||
import org.gradle.internal.logging.text.StyledTextOutput
|
||||
import org.gradle.internal.logging.text.StyledTextOutputFactory
|
||||
import org.gradle.kotlin.dsl.support.serviceOf
|
||||
import org.xml.sax.helpers.DefaultHandler
|
||||
|
||||
val out = project.serviceOf<StyledTextOutputFactory>().create("output")
|
||||
|
||||
/**
|
||||
* Finds the licenses for all of our dependencies and saves them to app/src/main/res/raw/third_party_licenses.
|
||||
*
|
||||
* This task will fail if we cannot map an artifact's license to a known license in [Licenses]. If this happens,
|
||||
* you need to manually save the new license, or map the URL to an existing license in [Licenses.getLicense].
|
||||
*/
|
||||
task("saveLicenses") {
|
||||
description = "Finds the licenses for all of our dependencies and saves them to app/src/main/res/raw/third_party_licenses."
|
||||
group = "Static Files"
|
||||
|
||||
doLast {
|
||||
// We resolve all the artifacts, map them to a dependency that lets us fetch the POM metadata, and then use that to generate our models
|
||||
val resolvedDependencies: List<ResolvedDependency> = configurations
|
||||
.asSequence()
|
||||
.filter { it.isCanBeResolved }
|
||||
.mapNotNull { it.tryResolveConfiguration() }
|
||||
.mapNotNull { it.tryToResolveArtifacts() }
|
||||
.flatten()
|
||||
.distinctBy { it.file }
|
||||
.map { pomDependency(it.id.componentIdentifier.toString()) }
|
||||
.mapNotNull { it.toResolvedDependency() }
|
||||
.filter { it.licenses.isNotEmpty() }
|
||||
.distinct()
|
||||
.toList()
|
||||
|
||||
// Next we want to map each dependency to a known license, failing if we can't do so
|
||||
val licenseToDependencies: MutableMap<LicenseData, MutableList<ResolvedDependency>> = mutableMapOf()
|
||||
|
||||
for (resolvedDependency in resolvedDependencies) {
|
||||
for (license in resolvedDependency.licenses) {
|
||||
val licenseData: LicenseData? = Licenses.getLicense(license.url)
|
||||
|
||||
if (licenseData == null) {
|
||||
printlnError("Failed to find matching license data for ${license.name} (${license.url}), which is in use by $resolvedDependency")
|
||||
throw RuntimeException("Failed to find matching license data! See output for details.")
|
||||
}
|
||||
|
||||
licenseToDependencies.getOrPut(licenseData) { mutableListOf() } += resolvedDependency
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can build the actual string that we'll write to the file
|
||||
val output = StringBuilder()
|
||||
|
||||
licenseToDependencies
|
||||
.entries
|
||||
.sortedByDescending { it.value.size }
|
||||
.forEach { entry ->
|
||||
val license: LicenseData = entry.key
|
||||
|
||||
// Some companies have multiple artifacts that are named identically (and licensed identically).
|
||||
// This just dedupes it based on the name+url so that you don't see the same name in the list twice.
|
||||
val dependencies: List<ResolvedDependency> = entry.value.distinctBy { it.name + it.url }.sortedBy { it.name }
|
||||
|
||||
output.append("The following dependencies are licensed under ${license.name}:\n\n")
|
||||
for (dependency in dependencies) {
|
||||
output.append("* ${dependency.name}")
|
||||
if (dependency.url != null) {
|
||||
output.append(" (${dependency.url})")
|
||||
}
|
||||
output.append("\n")
|
||||
}
|
||||
|
||||
output.append("\n")
|
||||
output.append("==========================================================\n")
|
||||
output.append("==========================================================\n")
|
||||
output.append(license.text)
|
||||
output.append("\n")
|
||||
output.append("==========================================================\n")
|
||||
output.append("==========================================================\n")
|
||||
output.append("\n")
|
||||
}
|
||||
|
||||
output.append("Kyber Patent License\n")
|
||||
output.append("https://csrc.nist.gov/csrc/media/Projects/post-quantum-cryptography/documents/selected-algos-2022/nist-pqc-license-summary-and-excerpts.pdf\n")
|
||||
output.append("\n")
|
||||
|
||||
// Save the file to disk
|
||||
rootProject
|
||||
.file("app/src/main/res/raw/third_party_licenses")
|
||||
.writeText(output.toString())
|
||||
|
||||
printlnSuccess("Done!")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a dependency for a POM to a [ResolvedDependency], which is just our nice and usable internal representation of all the data we need.
|
||||
*
|
||||
* @param leafDependency The actual dependency we're trying to resolve. If present, it means that [this] refers to a _parent_ of that dependency, which we're
|
||||
* looking at just to try to get licensing information.
|
||||
*/
|
||||
fun Dependency.toResolvedDependency(leafDependency: ResolvedDependency? = null): ResolvedDependency? {
|
||||
val pomConfiguration: Configuration = project.configurations.detachedConfiguration(this)
|
||||
val pomFile: File = try {
|
||||
pomConfiguration.resolve().first()
|
||||
} catch (e: Exception) {
|
||||
printlnWarning("[${this.id}] Failed to resolve the POM dependency to a file.")
|
||||
return null
|
||||
}
|
||||
|
||||
val xmlParser = XmlSlurper(true, false).apply {
|
||||
errorHandler = DefaultHandler()
|
||||
}
|
||||
|
||||
val xml: GPathResult = xmlParser.parse(pomFile)
|
||||
|
||||
val licenses: List<RawLicense> = try {
|
||||
xml
|
||||
.get("licenses")
|
||||
?.get("license")
|
||||
?.map { it as GPathResult }
|
||||
?.map {
|
||||
RawLicense(
|
||||
name = it.get("name")?.text()?.trim() ?: "",
|
||||
url = it.get("url")?.text()?.trim() ?: ""
|
||||
)
|
||||
}
|
||||
?.filter {
|
||||
it.name.isNotEmpty() && it.url.isNotEmpty()
|
||||
} ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
printlnWarning("[${this.id}] Error when parsing XML")
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// If we have a leafDependency, we just want to copy the possibly-found license into it, leaving the metadata alone, since that's the actual target of
|
||||
// our search
|
||||
val resolvedDependency = if (leafDependency != null) {
|
||||
leafDependency.copy(licenses = licenses)
|
||||
} else {
|
||||
ResolvedDependency(
|
||||
id = this.id,
|
||||
name = xml.get("name")?.text()?.ifBlank { null } ?: xml.get("artifactId")?.text()?.ifBlank { null } ?: this.name,
|
||||
url = xml.get("url")?.text()?.ifBlank { null },
|
||||
licenses = licenses
|
||||
)
|
||||
}
|
||||
|
||||
// If there's no licenses, but a parent exists, then we can walk up the tree and try to find a license in a parent
|
||||
if (resolvedDependency.licenses.isEmpty()) {
|
||||
val parentGroup: String? = xml.get("parent")?.get("groupId")?.text()?.trim()
|
||||
val parentName: String? = xml.get("parent")?.get("artifactId")?.text()?.trim()
|
||||
val parentVersion: String? = xml.get("parent")?.get("version")?.text()?.trim()
|
||||
|
||||
if (parentGroup != null && parentName != null && parentVersion != null) {
|
||||
printlnNormal("[${this.id}] Could not find a license on this node. Checking the parent.")
|
||||
return pomDependency("$parentGroup:$parentName:$parentVersion").toResolvedDependency(leafDependency = resolvedDependency)
|
||||
}
|
||||
} else if (leafDependency != null) {
|
||||
printlnNormal("[${leafDependency.id}] Found a license on a parent dependency. (parent = ${this.id})")
|
||||
}
|
||||
|
||||
return resolvedDependency
|
||||
}
|
||||
|
||||
fun printlnNormal(message: String) {
|
||||
out.style(StyledTextOutput.Style.Normal).println(message)
|
||||
}
|
||||
|
||||
fun printlnWarning(message: String) {
|
||||
out.style(StyledTextOutput.Style.Description).println(message)
|
||||
}
|
||||
|
||||
fun printlnSuccess(message: String) {
|
||||
out.style(StyledTextOutput.Style.SuccessHeader).println(message)
|
||||
}
|
||||
|
||||
fun printlnError(message: String) {
|
||||
out.style(StyledTextOutput.Style.FailureHeader).println(message)
|
||||
}
|
||||
|
||||
fun pomDependency(locator: String): Dependency {
|
||||
return project.dependencies.create("$locator@pom")
|
||||
}
|
||||
|
||||
fun Configuration.tryResolveConfiguration(): ResolvedConfiguration? {
|
||||
return try {
|
||||
this.resolvedConfiguration
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun ResolvedConfiguration.tryToResolveArtifacts(): Set<ResolvedArtifact>? {
|
||||
return try {
|
||||
this.resolvedArtifacts
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun GPathResult.get(key: String): GPathResult? {
|
||||
return this.getProperty(key) as? GPathResult
|
||||
}
|
||||
|
||||
val Dependency.id: String
|
||||
get() = "${this.group}:${this.name}:${this.version}"
|
||||
|
||||
data class ResolvedDependency(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val url: String?,
|
||||
val licenses: List<RawLicense>
|
||||
)
|
||||
|
||||
data class RawLicense(val name: String, val url: String)
|
||||
70
build-logic/plugins/src/main/java/signal-library.gradle.kts
Normal file
70
build-logic/plugins/src/main/java/signal-library.gradle.kts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.accessors.dm.LibrariesForTestLibs
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
val testLibs = the<LibrariesForTestLibs>()
|
||||
|
||||
val signalBuildToolsVersion: String by rootProject.extra
|
||||
val signalCompileSdkVersion: String by rootProject.extra
|
||||
val signalTargetSdkVersion: Int by rootProject.extra
|
||||
val signalMinSdkVersion: Int by rootProject.extra
|
||||
val signalJavaVersion: JavaVersion by rootProject.extra
|
||||
val signalKotlinJvmTarget: String by rootProject.extra
|
||||
|
||||
plugins {
|
||||
// We cannot use the version catalog in the plugins block in convention plugins (it's not supported).
|
||||
// Instead, plugin versions are controlled through the dependencies block in the build.gradle.kts.
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("ktlint")
|
||||
}
|
||||
|
||||
android {
|
||||
buildToolsVersion = signalBuildToolsVersion
|
||||
compileSdkVersion = signalCompileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdk = signalMinSdkVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = signalJavaVersion
|
||||
targetCompatibility = signalJavaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = signalKotlinJvmTarget
|
||||
suppressWarnings = true
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += "InvalidVectorPath"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
lintChecks(project(":lintchecks"))
|
||||
|
||||
coreLibraryDesugaring(libs.android.tools.desugar)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxjava)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
implementation(libs.kotlin.stdlib.jdk8)
|
||||
|
||||
ktlintRuleset(libs.ktlint.twitter.compose)
|
||||
|
||||
testImplementation(testLibs.junit.junit)
|
||||
testImplementation(testLibs.robolectric.robolectric)
|
||||
testImplementation(testLibs.androidx.test.core)
|
||||
testImplementation(testLibs.androidx.test.core.ktx)
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import org.gradle.accessors.dm.LibrariesForLibs
|
||||
import org.gradle.accessors.dm.LibrariesForTestLibs
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import org.gradle.kotlin.dsl.the
|
||||
|
||||
val libs = the<LibrariesForLibs>()
|
||||
val testLibs = the<LibrariesForTestLibs>()
|
||||
|
||||
val signalBuildToolsVersion: String by rootProject.extra
|
||||
val signalCompileSdkVersion: String by rootProject.extra
|
||||
val signalTargetSdkVersion: Int by rootProject.extra
|
||||
val signalMinSdkVersion: Int by rootProject.extra
|
||||
val signalJavaVersion: JavaVersion by rootProject.extra
|
||||
val signalKotlinJvmTarget: String by rootProject.extra
|
||||
|
||||
plugins {
|
||||
// We cannot use the version catalog in the plugins block in convention plugins (it's not supported).
|
||||
// Instead, plugin versions are controlled through the dependencies block in the build.gradle.kts.
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("ktlint")
|
||||
}
|
||||
|
||||
android {
|
||||
buildToolsVersion = signalBuildToolsVersion
|
||||
compileSdkVersion = signalCompileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
minSdk = signalMinSdkVersion
|
||||
targetSdk = signalTargetSdkVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = signalJavaVersion
|
||||
targetCompatibility = signalJavaVersion
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = signalKotlinJvmTarget
|
||||
suppressWarnings = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.android.tools.desugar)
|
||||
|
||||
implementation(project(":core-util"))
|
||||
|
||||
coreLibraryDesugaring(libs.android.tools.desugar)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxjava)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
implementation(libs.material.material)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.kotlin.stdlib.jdk8)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.material3)
|
||||
|
||||
ktlintRuleset(libs.ktlint.twitter.compose)
|
||||
|
||||
testImplementation(testLibs.junit.junit)
|
||||
testImplementation(testLibs.robolectric.robolectric)
|
||||
testImplementation(testLibs.androidx.test.core)
|
||||
testImplementation(testLibs.androidx.test.core.ktx)
|
||||
}
|
||||
128
build-logic/plugins/src/main/java/translations.gradle
Normal file
128
build-logic/plugins/src/main/java/translations.gradle
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import groovy.io.FileType
|
||||
import groovy.transform.stc.ClosureParams
|
||||
import groovy.transform.stc.SimpleType
|
||||
import org.signal.buildtools.StaticIpResolver
|
||||
|
||||
def allStringsResourceFiles(@ClosureParams(value = SimpleType.class, options = ['java.io.File']) Closure c) {
|
||||
file('src/main/res').eachFileRecurse(FileType.FILES) { f ->
|
||||
if (f.name == 'strings.xml') {
|
||||
c(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task replaceEllipsis {
|
||||
group 'Static Files'
|
||||
description 'Process strings for ellipsis characters.'
|
||||
doLast {
|
||||
allStringsResourceFiles { f ->
|
||||
def before = f.text
|
||||
def after = f.text.replace('...', '…')
|
||||
if (before != after) {
|
||||
f.text = after
|
||||
logger.info("$f.parentFile.name/$f.name...updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task cleanApostropheErrors {
|
||||
group 'Static Files'
|
||||
description 'Fix transifex apostrophe string errors.'
|
||||
doLast {
|
||||
allStringsResourceFiles { f ->
|
||||
def before = f.text
|
||||
def after = before.replaceAll(/([^\\=08])(')/, '$1\\\\\'')
|
||||
if (before != after) {
|
||||
f.text = after
|
||||
logger.info("$f.parentFile.name/$f.name...updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task excludeNonTranslatables {
|
||||
group 'Static Files'
|
||||
description 'Remove strings that are marked "translatable"="false" or are ExtraTranslations.'
|
||||
doLast {
|
||||
def englishFile = file('src/main/res/values/strings.xml')
|
||||
|
||||
def english = new XmlParser().parse(englishFile)
|
||||
def nonTranslatable = english
|
||||
.findAll { it['@translatable'] == 'false' }
|
||||
.collect { it['@name'] }
|
||||
.toSet()
|
||||
def all = english.collect { it['@name'] }.toSet()
|
||||
def translatable = all - nonTranslatable
|
||||
def inMultiline = false
|
||||
def endBlockName = ""
|
||||
|
||||
allStringsResourceFiles { f ->
|
||||
if (f != englishFile) {
|
||||
def newLines = f.readLines()
|
||||
.collect { line ->
|
||||
if (!inMultiline) {
|
||||
def singleLineMatcher = line =~ /name="([^"]*)".*(<\/|\/>)/
|
||||
if (singleLineMatcher.find()) {
|
||||
def name = singleLineMatcher.group(1)
|
||||
if (!line.contains('excludeNonTranslatables') && !translatable.contains(name)) {
|
||||
return " <!-- Removed by excludeNonTranslatables ${line.trim()} -->"
|
||||
}
|
||||
} else {
|
||||
def multilineStartMatcher = line =~ /<(.*) .?name="([^"]*)".*/
|
||||
if (multilineStartMatcher.find()) {
|
||||
endBlockName = multilineStartMatcher.group(1)
|
||||
def name = multilineStartMatcher.group(2)
|
||||
if (!line.contains('excludeNonTranslatables') && !translatable.contains(name)) {
|
||||
inMultiline = true;
|
||||
return " <!-- Removed by excludeNonTranslatables ${line.trim()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def multilineEndMatcher = line =~ /<\/${endBlockName}/
|
||||
if (multilineEndMatcher.find()) {
|
||||
inMultiline = false
|
||||
return "${line} -->"
|
||||
}
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
f.write(newLines.join("\n") + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task postTranslateQa {
|
||||
group 'Static Files'
|
||||
description 'Runs QA to check validity of updated strings, and ensure presence of any new languages in internal lists.'
|
||||
dependsOn ':qa'
|
||||
}
|
||||
|
||||
task resolveStaticIps {
|
||||
group 'Static Files'
|
||||
description 'Fetches static IPs for core hosts and writes them to static-ips.gradle'
|
||||
doLast {
|
||||
def staticIpResolver = new StaticIpResolver()
|
||||
new File(projectDir, "static-ips.gradle.kts").text = """
|
||||
rootProject.extra["service_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("chat.signal.org")}\"\"\"
|
||||
rootProject.extra["storage_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("storage.signal.org")}\"\"\"
|
||||
rootProject.extra["cdn_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("cdn.signal.org")}\"\"\"
|
||||
rootProject.extra["cdn2_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("cdn2.signal.org")}\"\"\"
|
||||
rootProject.extra["cdn3_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("cdn3.signal.org")}\"\"\"
|
||||
rootProject.extra["sfu_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("sfu.voip.signal.org")}\"\"\"
|
||||
rootProject.extra["content_proxy_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("contentproxy.signal.org")}\"\"\"
|
||||
rootProject.extra["svr2_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("svr2.signal.org")}\"\"\"
|
||||
rootProject.extra["cdsi_ips"] = \"\"\"${staticIpResolver.resolveToBuildConfig("cdsi.signal.org")}\"\"\"
|
||||
""".stripIndent().trim() + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
task updateStaticFilesAndQa {
|
||||
group 'Static Files'
|
||||
description 'Runs tasks to update static files. This includes translations, static IPs, and licenses. Runs QA afterwards to verify all went well. Intended to be run before cutting a release.'
|
||||
dependsOn replaceEllipsis, cleanApostropheErrors, excludeNonTranslatables, resolveStaticIps, postTranslateQa
|
||||
}
|
||||
29
build-logic/settings.gradle.kts
Normal file
29
build-logic/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
create("testLibs") {
|
||||
from(files("../gradle/test-libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "build-logic"
|
||||
|
||||
include(":plugins")
|
||||
include(":tools")
|
||||
33
build-logic/tools/build.gradle.kts
Normal file
33
build-logic/tools/build.gradle.kts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
plugins {
|
||||
alias(libs.plugins.jetbrains.kotlin.jvm)
|
||||
id("java-library")
|
||||
alias(libs.plugins.ktlint)
|
||||
}
|
||||
|
||||
val signalJavaVersion: JavaVersion by rootProject.extra
|
||||
val signalKotlinJvmTarget: String by rootProject.extra
|
||||
|
||||
java {
|
||||
sourceCompatibility = signalJavaVersion
|
||||
targetCompatibility = signalJavaVersion
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(signalKotlinJvmTarget)
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: For now, in order to run ktlint on this project, you have to manually run ./gradlew :build-logic:tools:ktlintFormat
|
||||
// Gotta figure out how to get it auto-included in the normal ./gradlew ktlintFormat
|
||||
ktlint {
|
||||
version.set("1.2.1")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(gradleApi())
|
||||
|
||||
implementation(libs.dnsjava)
|
||||
testImplementation(testLibs.junit.junit)
|
||||
testImplementation(testLibs.mockk)
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package org.signal.buildtools
|
||||
|
||||
import org.xbill.DNS.ARecord
|
||||
import org.xbill.DNS.Lookup
|
||||
import org.xbill.DNS.Record
|
||||
import org.xbill.DNS.SimpleResolver
|
||||
import org.xbill.DNS.Type
|
||||
import java.net.UnknownHostException
|
||||
import kotlin.streams.toList
|
||||
|
||||
/**
|
||||
* A tool to resolve hostname to static IPs.
|
||||
* Feeds into our custom DNS resolver to provide a static IP fallback for our services.
|
||||
*/
|
||||
class StaticIpResolver @JvmOverloads constructor(
|
||||
private val recordFetcher: RecordFetcher = RealRecordFetcher
|
||||
) {
|
||||
|
||||
/**
|
||||
* Resolves a hostname to a list of IPs, represented as a Java array declaration. e.g.
|
||||
*
|
||||
* ```java
|
||||
* new String[]{"192.168.1.1", "192.168.1.2"}
|
||||
* ```
|
||||
*
|
||||
* This is intended to be injected as a BuildConfig.
|
||||
*/
|
||||
fun resolveToBuildConfig(hostName: String): String {
|
||||
val ips: List<String> = resolve(hostName)
|
||||
val builder = StringBuilder()
|
||||
|
||||
builder.append("new String[]{")
|
||||
|
||||
ips.forEachIndexed { i, ip ->
|
||||
builder.append("\"").append(ip).append("\"")
|
||||
|
||||
if (i < ips.size - 1) {
|
||||
builder.append(",")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.append("}").toString()
|
||||
}
|
||||
|
||||
private fun resolve(hostname: String): List<String> {
|
||||
val ips: MutableSet<String> = mutableSetOf()
|
||||
|
||||
// Run several resolves to mitigate DNS round robin
|
||||
for (i in 1..10) {
|
||||
ips.addAll(resolveOnce(hostname))
|
||||
}
|
||||
|
||||
return ips.stream().sorted().toList()
|
||||
}
|
||||
|
||||
private fun resolveOnce(hostName: String): List<String> {
|
||||
try {
|
||||
val records = recordFetcher.fetchRecords(hostName)
|
||||
if (records != null) {
|
||||
return records
|
||||
.filter { it.type == Type.A }
|
||||
.map { it as ARecord }
|
||||
.map { it.address }
|
||||
.map { it.hostAddress }
|
||||
.filterNotNull()
|
||||
} else {
|
||||
throw IllegalStateException("Failed to resolve host! Lookup did not return any records.. $hostName")
|
||||
}
|
||||
} catch (e: UnknownHostException) {
|
||||
throw IllegalStateException("Failed to resolve host! $hostName", e)
|
||||
}
|
||||
}
|
||||
|
||||
interface RecordFetcher {
|
||||
fun fetchRecords(hostName: String): Array<Record>?
|
||||
}
|
||||
|
||||
private object RealRecordFetcher : RecordFetcher {
|
||||
override fun fetchRecords(hostName: String): Array<Record>? {
|
||||
val resolver = SimpleResolver("1.1.1.1")
|
||||
|
||||
resolver.setTCP(true)
|
||||
|
||||
val lookup: Lookup = doLookup(hostName)
|
||||
|
||||
lookup.setResolver(resolver)
|
||||
|
||||
return lookup.run()
|
||||
}
|
||||
|
||||
@Throws(UnknownHostException::class)
|
||||
private fun doLookup(hostname: String): Lookup {
|
||||
try {
|
||||
return Lookup(hostname)
|
||||
} catch (e: Throwable) {
|
||||
throw UnknownHostException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package org.signal.buildtools
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.xbill.DNS.ARecord
|
||||
import org.xbill.DNS.DClass
|
||||
import org.xbill.DNS.Name
|
||||
import org.xbill.DNS.Record
|
||||
import java.net.Inet4Address
|
||||
|
||||
class StaticIpResolverTest {
|
||||
|
||||
companion object {
|
||||
const val SIGNAL_DOT_ORG = "www.signal.org"
|
||||
val SIGNAL_IP = byteArrayOf(123, 45, 67, 89)
|
||||
val STRINGIFIED_IP = SIGNAL_IP.joinToString(".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given a hostname with records, when I resolveToBuildConfig, then I expect a matching IP`() {
|
||||
val staticIpResolver = StaticIpResolver(
|
||||
FakeRecordFetcher(
|
||||
mapOf(
|
||||
SIGNAL_DOT_ORG to arrayOf(
|
||||
ARecord(
|
||||
Name.fromString("www."),
|
||||
DClass.ANY,
|
||||
0L,
|
||||
mockk<Inet4Address> {
|
||||
every { address } returns SIGNAL_IP
|
||||
every { hostAddress } returns STRINGIFIED_IP
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val actual = staticIpResolver.resolveToBuildConfig(SIGNAL_DOT_ORG)
|
||||
val expected = """
|
||||
new String[]{"$STRINGIFIED_IP"}
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `Given a hostname without records, when I resolveToBuildConfig, then I expect`() {
|
||||
val staticIpResolver = StaticIpResolver(FakeRecordFetcher(emptyMap()))
|
||||
staticIpResolver.resolveToBuildConfig(SIGNAL_DOT_ORG)
|
||||
}
|
||||
|
||||
private class FakeRecordFetcher(private val recordMap: Map<String, Array<Record>?>) : StaticIpResolver.RecordFetcher {
|
||||
override fun fetchRecords(hostName: String): Array<Record>? {
|
||||
return recordMap[hostName]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue