Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions buildSrc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# buildSrc

This directory contains shared build logic for the Probe multiplatform project.

## What's Included

### Core Classes
- **`AppConfig`** – Data class defining app configuration for different variants (OONI, DW, etc.)
- **`Organization`** – Sealed class representing build flavors/organizations, each with its own `AppConfig`

### Utilities
- **`BuildUtils`** – Utility functions for build scripts:
- `isFdroidTaskRequested()` – Check if F-Droid build task is requested
- `isDebugTaskRequested()` – Check if Debug build task is requested
- Other helpers for platform suffixes, file copying, and .gitignore management
- **`TaskRegistration`** – Functions for registering custom Gradle tasks (Android, Desktop, Resources, etc.)

### Plugins
- **`ConfigurationPlugin`** – Main plugin for common configuration and automatic task registration

## How It Works
- The plugins and utilities in this directory are used to standardize build logic, configuration, and task setup across all multiplatform modules.
- Organization-specific configuration is handled via the `Organization` sealed class and its associated `AppConfig`.
- Custom tasks are registered using functions in `TaskRegistration`.
18 changes: 18 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
`kotlin-dsl`
}

repositories {
google()
gradlePluginPortal()
mavenCentral()
}

gradlePlugin {
plugins {
create("commonConfiguration") {
id = "ooni.common"
implementationClass = "ConfigurationPlugin"
}
}
}
10 changes: 10 additions & 0 deletions buildSrc/src/main/kotlin/AppConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Configuration data class for different app variants.
*/
data class AppConfig(
val appId: String,
val appName: String,
val folder: String,
val supportsOoniRun: Boolean = false,
val supportedLanguages: List<String>,
)
91 changes: 91 additions & 0 deletions buildSrc/src/main/kotlin/BuildUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import org.gradle.api.Project
import org.gradle.internal.os.OperatingSystem
import java.io.File

/**
* Check if F-Droid build task is requested.
*/
fun Project.isFdroidTaskRequested(): Boolean =
gradle.startParameter.taskRequests
.flatMap { it.args }
.any { it.contains("Fdroid") }

/**
* Check if Debug build task is requested.
*/
fun Project.isDebugTaskRequested(): Boolean =
gradle.startParameter.taskRequests
.flatMap { it.args }
.any { it.contains("Debug") }

/**
* Get the appropriate JavaFX suffix for the current OS and architecture.
*/
fun getJavaFxSuffix(): String {
val os = OperatingSystem.current()
val arch = System.getProperty("os.arch")
return when {
os.isMacOsX -> if (arch == "aarch64") "mac-aarch64" else "mac"
os.isWindows -> "win"
os.isLinux -> if (arch == "aarch64") "linux-aarch64" else "linux"
else -> throw IllegalStateException("Unknown OS: $os")
}
}

/**
* Add a line to .gitignore if it doesn't already exist.
*/
fun ignoreCopiedFileIfNotIgnored(
gitignorePath: String,
lineToAdd: String,
) {
val file = File(gitignorePath)
if (!file.exists()) {
file.createNewFile()
}

val fileContents = file.readText()

if (!fileContents.contains(lineToAdd)) {
file.appendText("\n$lineToAdd")
}
}

/**
* Copy files from one directory to another recursively.
*
* @param from The source directory.
* @param to The destination directory.
*/
fun copyRecursive(
from: File,
to: File,
) {
if (!from.exists()) {
println("Source directory does not exist: $from")
return
}
from.listFiles()?.forEach { file ->
if (file.name != ".DS_Store") {
if (file.isDirectory) {
val newDir = File(to, file.name)
newDir.mkdir()
copyRecursive(file, newDir)
} else {
val destinationFile = File(to, file.name)
if (destinationFile.exists()) {
destinationFile.delete()
}
if (!destinationFile.parentFile.exists()) {
destinationFile.parentFile.mkdirs()
}
file.copyTo(destinationFile).also {
ignoreCopiedFileIfNotIgnored(
to.absolutePath + "/.gitignore",
it.name,
)
}
}
}
}
}
22 changes: 22 additions & 0 deletions buildSrc/src/main/kotlin/ConfigurationPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import org.gradle.api.Plugin
import org.gradle.api.Project

/**
* Common configuration plugin for projects.
*/
class ConfigurationPlugin : Plugin<Project> {
override fun apply(project: Project) {
with(project) {
// Common configuration that applies to all projects
group = "org.ooni.probe"
version = project.findProperty("version") ?: "1.0.0"

val organization = project.findProperty("organization") as? String
val config = Organization.fromKey(organization).config

registerTasks(config)

configureTasks()
}
}
}
62 changes: 62 additions & 0 deletions buildSrc/src/main/kotlin/Organization.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Represents the distinct build flavors or organizations.
* Using an enum ensures type safety when accessing configurations.
*
* @param key The string value used as a key in the configuration map.
* @param config The AppConfig associated with the organization.
*/
sealed class Organization(val key: String, val config: AppConfig) {
object Ooni : Organization(
"ooni",
AppConfig(
appId = "org.openobservatory.ooniprobe",
appName = "OONI Probe",
folder = "ooniMain",
supportsOoniRun = true,
supportedLanguages = listOf(
"ar",
"ca",
"de",
"el",
"es",
"fa",
"fr",
"hi",
"id",
"is",
"it",
"my",
"nl",
"pt-rBR",
"ro",
"ru",
"sk",
"sq",
"sw",
"th",
"tr",
"vi",
"zh-rCN",
"zh-rTW"
),
)
)

object Dw : Organization(
"dw",
AppConfig(
appId = "com.dw.ooniprobe",
appName = "News Media Scan",
folder = "dwMain",
supportsOoniRun = false,
supportedLanguages = listOf(
"de", "es", "fr", "pt-rBR", "ru", "tr"
),
)
)

companion object {
private val keyMap = listOf(Ooni, Dw).associateBy { it.key }
fun fromKey(key: String?): Organization = keyMap[key] ?: Ooni
}
}
149 changes: 149 additions & 0 deletions buildSrc/src/main/kotlin/TaskRegistration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import org.gradle.api.Project
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.JavaExec
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType
import java.io.File
import kotlin.let

/**
* Registers all custom tasks for the project
*/
fun Project.registerTasks(config: AppConfig) {
registerAndroidTasks(config)
registerDesktopTasks()
registerResourceTasks(config)
configureTaskDependencies()
}

private fun Project.registerAndroidTasks(config: AppConfig) {
tasks.register("runDebug", Exec::class) {
group = "ooni"
description = "Clean, install and run the debug variant"
dependsOn("clean", "installFullDebug")
commandLine(
"adb",
"shell",
"am",
"start",
"-n",
"${config.appId}.dev/org.ooni.probe.MainActivity",
)
}
}

private fun Project.registerDesktopTasks() {
tasks.register("makeLibrary", Exec::class) {
group = "ooni"
description = "Build native libraries (NetworkTypeFinder and UpdateBridge)"
workingDir = file("src/desktopMain")
commandLine = listOf("make", "all")
doFirst {
println("🔨 Building native libraries...")
}
doLast {
println("✅ Native libraries built successfully")
}
}

tasks.register("cleanLibrary", Exec::class) {
group = "ooni"
description = "Clean native library build artifacts"
workingDir = file("src/desktopMain")
commandLine = listOf("make", "clean")
}
}

private fun Project.registerResourceTasks(config: AppConfig) {
tasks.register("copyBrandingToCommonResources") {
group = "ooni"
description = "Copy branding resources to common resources directory"
doLast {
val projectDir = project.projectDir.absolutePath
copyRecursive(
from = File(projectDir, "src/${config.folder}/res"),
to = File(projectDir, "src/commonMain/res"),
)
copyRecursive(
from = File(projectDir, "src/${config.folder}/composeResources"),
to = File(projectDir, "src/commonMain/composeResources"),
)
}
}

tasks.register("cleanCopiedCommonResourcesToFlavor") {
group = "ooni"
description = "Clean copied common resources from flavor directories"
doLast {
val projectDir = project.projectDir.absolutePath

deleteFilesFromGitIgnore("$projectDir/src/commonMain/res")
deleteFilesFromGitIgnore("$projectDir/src/commonMain/resources")
deleteFilesFromGitIgnore("$projectDir/src/commonMain/composeResources")
}
}
}

private fun Project.configureTaskDependencies() {
// Configure existing tasks with dependencies after evaluation
afterEvaluate {
tasks.findByName("compileKotlinDesktop")?.dependsOn?.add("makeLibrary")

tasks.findByName("preBuild")?.dependsOn("copyBrandingToCommonResources")

tasks.findByName("clean")?.dependsOn("copyBrandingToCommonResources", "cleanCopiedCommonResourcesToFlavor")
}
}

/**
* Configures existing tasks with OONI-specific settings
*/
fun Project.configureTasks() {
configureJavaExecTasks()
}

private fun Project.configureJavaExecTasks() {
tasks.withType<JavaExec> {
systemProperty(
"java.library.path",
"$projectDir/src/desktopMain/resources/macos" +
File.pathSeparator +
"$projectDir/src/desktopMain/resources/windows" +
File.pathSeparator +
"$projectDir/src/desktopMain/resources/linux" +
File.pathSeparator +
System.getProperty("java.library.path"),
)

// Get desktop updates public key from project properties
project.findProperty("desktopUpdatesPublicKey")?.let { key ->
systemProperty("desktopUpdatesPublicKey", key)
}
}
}


/**
* Configure tasks to delete files from .gitignore.
* This version matches the original implementation from the build script.
*/
fun deleteFilesFromGitIgnore(folderPath: String) {
val destinationFile = File(folderPath)
destinationFile.listFiles()?.forEach { folder ->
folder.listFiles()?.forEach { file ->
if (file.name == ".gitignore") {
file
.readText()
.lines()
.forEach { line ->
if (line.isNotEmpty()) {
println("Removing $line")
File(folder, line).deleteRecursively()
}
}.also {
file.delete()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
implementation-class=ConfigurationPlugin
Loading
Loading