Skip to content

Instantly share code, notes, and snippets.

@Aidanvii7
Last active July 26, 2024 13:24
Show Gist options
  • Save Aidanvii7/76e961d8915cd92d4a0fbda39ccf93f8 to your computer and use it in GitHub Desktop.
Save Aidanvii7/76e961d8915cd92d4a0fbda39ccf93f8 to your computer and use it in GitHub Desktop.
FirebaseIosPlugin example
// File name should be build.gradle.kts, but GitHub won't format it correctly
// This should be in your build-logic/conventions/build.gradle.kts (see nowinandroid for setup https://github.com/android/nowinandroid/tree/main/build-logic)
gradlePlugin {
plugins {
register("FirebaseIosPlugin") {
id = "firebase.ios"
implementationClass = "com.yourapp.plugins.FirebaseIosPlugin"
}
}
}
package com.yourapp.plugins
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Property
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.DefaultCInteropSettings
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.konan.target.Family.IOS
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
/**
* pluginId is 'firebase.ios'
* For this to work, you need to:
* 1. XCode->settings->locations
* 2. change DerivedData from default to relative
* 3. advanced->custom->relative to workspace (products and intermediates field should be automatically filled)
* 4. XCode->File->add package dependencies
* 2. paste https://github.com/firebase/firebase-ios-sdk.git into search bar
* 3. select FirebaseAnalytics
* 4. go to build phases and add whatever else you need from Firebase (in my case FirebaseMessaging, FirebaseCrashlytics & FirebaseRemoteConfig), this will update your project.pbxproj file
*
* For CI, you will need to override the DerivedData path in the xcodebuild command, see https://stackoverflow.com/questions/33044633/command-line-option-to-change-xcode-deriveddata-location/33599316#33599316
* I think this should work but not yet tested: xcodebuild -derivedDataPath . (with dot)
*/
class FirebaseIosPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
configureExtension<KotlinMultiplatformExtension> KotlinMultiplatformExtension@{
asExtensionAware().registerExtension<IosFirebaseExtension>(
project = this@with,
this@KotlinMultiplatformExtension,
) {
configureSourceSetsOn(this@with)
}
}
}
private fun IosFirebaseExtension.configureSourceSetsOn(target: Project) = with(target) {
with(kotlinMultiplatformExtension) {
sourceSets {
val sourcePackagesRoot = File(project.projectDir.parentFile!!, "iosApp/DerivedData/iosApp/SourcePackages")
val firebaseIosSdkCheckoutsLocation = File(sourcePackagesRoot, "checkouts/firebase-ios-sdk")
val firebaseIosSdkArtifactsLocation = File(sourcePackagesRoot, "artifacts/firebase-ios-sdk")
val firebaseCoreHeader = File(firebaseIosSdkCheckoutsLocation, "FirebaseCore/Sources/Public/FirebaseCore/FirebaseCore.h")
val firebaseCrashlyticsHeader =
File(firebaseIosSdkCheckoutsLocation, "Crashlytics/Crashlytics/Public/FirebaseCrashlytics/FirebaseCrashlytics.h")
val firebaseMessagingHeader =
File(firebaseIosSdkCheckoutsLocation, "FirebaseMessaging/Sources/Public/FirebaseMessaging/FirebaseMessaging.h")
val firebaseAnalyticsHeaderArm64 = File(
firebaseIosSdkArtifactsLocation,
"FirebaseAnalytics/FirebaseAnalytics.xcframework/ios-arm64/FirebaseAnalytics.framework/Headers/FirebaseAnalytics.h"
)
val firebaseAnalyticsHeaderArm64x86Simulator = File(
firebaseIosSdkArtifactsLocation,
"FirebaseAnalytics/FirebaseAnalytics.xcframework/ios-arm64_x86_64-simulator/FirebaseAnalytics.framework/Headers/FirebaseAnalytics.h"
)
val firebaseCoreAction: DefinitionFileAction = getDefinitionFileActionFor(
headerFile = firebaseCoreHeader,
packageName = "com.google.firebase.core",
flag = core,
)
val firebaseCrashlyticsAction: DefinitionFileAction = getDefinitionFileActionFor(
headerFile = firebaseCrashlyticsHeader,
packageName = "com.google.firebase.crashlytics",
flag = crashlytics,
)
val firebaseMessagingAction: DefinitionFileAction = getDefinitionFileActionFor(
headerFile = firebaseMessagingHeader,
packageName = "com.google.firebase.messaging",
flag = messaging,
)
val firebaseAnalyticsArm64Action: DefinitionFileAction = getDefinitionFileActionFor(
headerFile = firebaseAnalyticsHeaderArm64,
packageName = "com.google.firebase.analytics",
flag = analytics,
)
val firebaseAnalyticsArm64x86SimulatorAction: DefinitionFileAction = getDefinitionFileActionFor(
headerFile = firebaseAnalyticsHeaderArm64x86Simulator,
packageName = "com.google.firebase.analytics",
flag = analytics,
)
with(iosTargets) {
forEach { target: KotlinNativeTarget ->
with(target.compilations.getByName("main").cinterops) {
firebaseCoreAction(this)
firebaseCrashlyticsAction(this)
firebaseMessagingAction(this)
when (val konanTarget: KonanTarget = target.konanTarget) {
KonanTarget.IOS_ARM64 -> firebaseAnalyticsArm64Action(this)
KonanTarget.IOS_SIMULATOR_ARM64,
KonanTarget.IOS_X64 -> firebaseAnalyticsArm64x86SimulatorAction(this)
else -> TODO("Unsupported target: $konanTarget")
}
}
}
}
}
}
}
private fun Project.getDefinitionFileActionFor(
headerFile: File,
packageName: String,
flag: Property<Boolean>,
): DefinitionFileAction {
val definitionFile: File = project.layout.buildDirectory.file("generated/cinterop/firebase/${headerFile.nameWithoutExtension}.def").get().asFile
return if (flag.getOrElse(false)) {
check(headerFile.exists()) {
"${headerFile.name} does not exist, please add the firebase-ios-sdk via XCode (https://github.com/firebase/firebase-ios-sdk.git)"
}
DefinitionFileAction.Regenerate(
headerFile = headerFile,
definitionFile = definitionFile,
packageName = packageName
)
} else {
DefinitionFileAction.Delete(
definitionFile = definitionFile
)
}
}
abstract class IosFirebaseExtension(
internal val kotlinMultiplatformExtension: KotlinMultiplatformExtension,
) {
internal abstract val core: Property<Boolean>
internal abstract val crashlytics: Property<Boolean>
internal abstract val analytics: Property<Boolean>
internal abstract val messaging: Property<Boolean>
fun includeCore() {
core.set(true)
}
fun includeCrashlytics() {
crashlytics.set(true)
}
fun includeAnalytics() {
analytics.set(true)
}
fun includeMessaging() {
messaging.set(true)
}
}
sealed interface DefinitionFileAction {
val definitionFile: File
operator fun invoke(receiver: NamedDomainObjectContainer<DefaultCInteropSettings>)
class Delete(
override val definitionFile: File,
) : DefinitionFileAction {
override fun invoke(receiver: NamedDomainObjectContainer<DefaultCInteropSettings>) = with(receiver) {
if (definitionFile.exists()) {
definitionFile.delete()
}
}
}
class Regenerate(
private val headerFile: File,
override val definitionFile: File,
private val packageName: String,
) : DefinitionFileAction {
override operator fun invoke(receiver: NamedDomainObjectContainer<DefaultCInteropSettings>) = with(receiver) {
writeDefinitionFile()
createCInterop()
}
private fun writeDefinitionFile() {
with(definitionFile) {
val definitionFileContents: String = buildString {
appendLine("package = $packageName")
appendLine("language = Objective-C")
appendLine("headers = ${headerFile.name}")
}
if (exists() && readText() == definitionFileContents) {
return
}
parentFile!!.mkdirs()
println("writing ${definitionFile.name} at ${definitionFile.absolutePath} for ${headerFile.name} at ${headerFile.absolutePath}:")
println(definitionFileContents)
writeText(definitionFileContents)
}
}
context(NamedDomainObjectContainer<DefaultCInteropSettings>)
private fun createCInterop() {
check(headerFile.exists()) {
"${headerFile.name} does not exist, please add the firebase-ios-sdk via XCode (https://github.com/firebase/firebase-ios-sdk.git)"
}
check(definitionFile.exists()) {
"${definitionFile.name} does not exist"
}
println("definition contents: ${definitionFile.readText()}")
create(headerFile.nameWithoutExtension.lowercase()) {
val headerDirectory: File = headerFile.parentFile
println("headerDirectory: $headerDirectory")
includeDirs(headerDirectory)
this.definitionFile.set(this@Regenerate.definitionFile)
compilerOpts("-DNS_FORMAT_ARGUMENT(A)=", "-D_Nullable_result=_Nullable")
}
}
}
}
}
fun KotlinMultiplatformExtension.sourceSets(configure: Action<NamedDomainObjectContainer<KotlinSourceSet>>): Unit =
asExtensionAware().extensions.configure("sourceSets", configure)
fun Any.asExtensionAware(): ExtensionAware = this as ExtensionAware
inline fun <reified T : Any> ExtensionAware.configureExtension(noinline configuration: T.() -> Unit) {
extensions.configure<T>(configuration)
}
val KotlinMultiplatformExtension.iosTargets: List<KotlinNativeTarget>
get() = targets
.filterIsInstance<KotlinNativeTarget>()
.filter { target: KotlinNativeTarget ->
target.konanTarget.family == IOS
}
inline fun <reified T : Any> ExtensionAware.registerExtension(
project: Project,
vararg constructionArguments: Any?,
noinline onAfterEvaluate: (T.() -> Unit)? = null,
): T = with(project){
val extensionName: String = getExtensionNameFromClassName<T>()
val createdExtension: T = this.extensions.create(extensionName, T::class.java, *constructionArguments)
println("extension for ${T::class.simpleName} registered as $extensionName")
if (onAfterEvaluate != null) {
afterEvaluate {
createdExtension.onAfterEvaluate()
}
}
return createdExtension
}
inline fun <reified T : Any> getExtensionNameFromClassName(): String = requireNotNull(T::class.simpleName) {
"T cannot be an anonymous class"
}.toCamelCase().removeSuffix("Extension")
fun String.toCamelCase(): String = if (isEmpty()) this else this[0].lowercaseChar() + substring(1)
[plugins]
firebase-ios = { id = "firebase.ios", version = "1" }
// this is the build script of the consuming module (also should be build.gradle.kts)
plugins {
// TODO: apply kotlin multiplatform plugin from Jetbrains etc..
alias(libs.plugins.firebase.ios)
}
kotlin {
// TODO: specify multiplatform targets etc
iosFirebase {
includeCore()
includeCrashlytics()
includeAnalytics()
includeMessaging()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment