diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index f214f13f..17db5d1d 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -25,6 +25,10 @@ jobs: restore-keys: | ${{ runner.os }}-konan- + - name: dconf install + run: | + sudo apt-get install libdconf-dev + - name: Linux build run: | ./gradlew build publishToMavenLocal --no-daemon --stacktrace diff --git a/multiplatform-settings/api/multiplatform-settings.klib.api b/multiplatform-settings/api/multiplatform-settings.klib.api index c8c4ee74..7d4ac5d6 100644 --- a/multiplatform-settings/api/multiplatform-settings.klib.api +++ b/multiplatform-settings/api/multiplatform-settings.klib.api @@ -225,6 +225,44 @@ final class com.russhwolf.settings/StorageSettings : com.russhwolf.settings/Sett final fun remove(kotlin/String) // com.russhwolf.settings/StorageSettings.remove|remove(kotlin.String){}[0] } +// Targets: [linuxX64] +final class com.russhwolf.settings/DConfSettings : com.russhwolf.settings/Settings { // com.russhwolf.settings/DConfSettings|null[0] + constructor (kotlin/String) // com.russhwolf.settings/DConfSettings.|(kotlin.String){}[0] + + final val keys // com.russhwolf.settings/DConfSettings.keys|{}keys[0] + final fun (): kotlin.collections/Set // com.russhwolf.settings/DConfSettings.keys.|(){}[0] + final val size // com.russhwolf.settings/DConfSettings.size|{}size[0] + final fun (): kotlin/Int // com.russhwolf.settings/DConfSettings.size.|(){}[0] + + final fun clear() // com.russhwolf.settings/DConfSettings.clear|clear(){}[0] + final fun getBoolean(kotlin/String, kotlin/Boolean): kotlin/Boolean // com.russhwolf.settings/DConfSettings.getBoolean|getBoolean(kotlin.String;kotlin.Boolean){}[0] + final fun getBooleanOrNull(kotlin/String): kotlin/Boolean? // com.russhwolf.settings/DConfSettings.getBooleanOrNull|getBooleanOrNull(kotlin.String){}[0] + final fun getDouble(kotlin/String, kotlin/Double): kotlin/Double // com.russhwolf.settings/DConfSettings.getDouble|getDouble(kotlin.String;kotlin.Double){}[0] + final fun getDoubleOrNull(kotlin/String): kotlin/Double? // com.russhwolf.settings/DConfSettings.getDoubleOrNull|getDoubleOrNull(kotlin.String){}[0] + final fun getFloat(kotlin/String, kotlin/Float): kotlin/Float // com.russhwolf.settings/DConfSettings.getFloat|getFloat(kotlin.String;kotlin.Float){}[0] + final fun getFloatOrNull(kotlin/String): kotlin/Float? // com.russhwolf.settings/DConfSettings.getFloatOrNull|getFloatOrNull(kotlin.String){}[0] + final fun getInt(kotlin/String, kotlin/Int): kotlin/Int // com.russhwolf.settings/DConfSettings.getInt|getInt(kotlin.String;kotlin.Int){}[0] + final fun getIntOrNull(kotlin/String): kotlin/Int? // com.russhwolf.settings/DConfSettings.getIntOrNull|getIntOrNull(kotlin.String){}[0] + final fun getLong(kotlin/String, kotlin/Long): kotlin/Long // com.russhwolf.settings/DConfSettings.getLong|getLong(kotlin.String;kotlin.Long){}[0] + final fun getLongOrNull(kotlin/String): kotlin/Long? // com.russhwolf.settings/DConfSettings.getLongOrNull|getLongOrNull(kotlin.String){}[0] + final fun getString(kotlin/String, kotlin/String): kotlin/String // com.russhwolf.settings/DConfSettings.getString|getString(kotlin.String;kotlin.String){}[0] + final fun getStringOrNull(kotlin/String): kotlin/String? // com.russhwolf.settings/DConfSettings.getStringOrNull|getStringOrNull(kotlin.String){}[0] + final fun hasKey(kotlin/String): kotlin/Boolean // com.russhwolf.settings/DConfSettings.hasKey|hasKey(kotlin.String){}[0] + final fun putBoolean(kotlin/String, kotlin/Boolean) // com.russhwolf.settings/DConfSettings.putBoolean|putBoolean(kotlin.String;kotlin.Boolean){}[0] + final fun putDouble(kotlin/String, kotlin/Double) // com.russhwolf.settings/DConfSettings.putDouble|putDouble(kotlin.String;kotlin.Double){}[0] + final fun putFloat(kotlin/String, kotlin/Float) // com.russhwolf.settings/DConfSettings.putFloat|putFloat(kotlin.String;kotlin.Float){}[0] + final fun putInt(kotlin/String, kotlin/Int) // com.russhwolf.settings/DConfSettings.putInt|putInt(kotlin.String;kotlin.Int){}[0] + final fun putLong(kotlin/String, kotlin/Long) // com.russhwolf.settings/DConfSettings.putLong|putLong(kotlin.String;kotlin.Long){}[0] + final fun putString(kotlin/String, kotlin/String) // com.russhwolf.settings/DConfSettings.putString|putString(kotlin.String;kotlin.String){}[0] + final fun remove(kotlin/String) // com.russhwolf.settings/DConfSettings.remove|remove(kotlin.String){}[0] + + final class Factory : com.russhwolf.settings/Settings.Factory { // com.russhwolf.settings/DConfSettings.Factory|null[0] + constructor (kotlin/String) // com.russhwolf.settings/DConfSettings.Factory.|(kotlin.String){}[0] + + final fun create(kotlin/String?): com.russhwolf.settings/Settings // com.russhwolf.settings/DConfSettings.Factory.create|create(kotlin.String?){}[0] + } +} + // Targets: [mingwX64] final class com.russhwolf.settings/RegistrySettings : com.russhwolf.settings/Settings { // com.russhwolf.settings/RegistrySettings|null[0] constructor (kotlin/String) // com.russhwolf.settings/RegistrySettings.|(kotlin.String){}[0] diff --git a/multiplatform-settings/build.gradle.kts b/multiplatform-settings/build.gradle.kts index 5fe120a8..47208b49 100644 --- a/multiplatform-settings/build.gradle.kts +++ b/multiplatform-settings/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi - /* * Copyright 2019 Russell Wolf * @@ -15,6 +13,8 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi * See the License for the specific language governing permissions and * limitations under the License. */ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { id("standard-configuration") @@ -26,6 +26,9 @@ standardConfig { } kotlin { + targets.getByName("linuxX64") { + compilations["main"].cinterops.create("dconf") + } @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") diff --git a/multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt b/multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt new file mode 100644 index 00000000..6a39a9e2 --- /dev/null +++ b/multiplatform-settings/src/linuxX64Main/kotlin/com/russhwolf/settings/DConfSettings.kt @@ -0,0 +1,192 @@ +/* + * Copyright 2024 Russell Wolf + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.russhwolf.settings + +import com.russhwolf.settings.cinterop.dconf.DConfClient +import com.russhwolf.settings.cinterop.dconf.FALSE +import com.russhwolf.settings.cinterop.dconf.GError +import com.russhwolf.settings.cinterop.dconf.GVariant +import com.russhwolf.settings.cinterop.dconf.dconf_client_list +import com.russhwolf.settings.cinterop.dconf.dconf_client_new +import com.russhwolf.settings.cinterop.dconf.dconf_client_read +import com.russhwolf.settings.cinterop.dconf.dconf_client_sync +import com.russhwolf.settings.cinterop.dconf.dconf_client_write_sync +import com.russhwolf.settings.cinterop.dconf.dconf_is_rel_key +import com.russhwolf.settings.cinterop.dconf.g_object_ref +import com.russhwolf.settings.cinterop.dconf.g_object_unref +import com.russhwolf.settings.cinterop.dconf.g_variant_get_boolean +import com.russhwolf.settings.cinterop.dconf.g_variant_get_double +import com.russhwolf.settings.cinterop.dconf.g_variant_get_int32 +import com.russhwolf.settings.cinterop.dconf.g_variant_get_int64 +import com.russhwolf.settings.cinterop.dconf.g_variant_get_string +import com.russhwolf.settings.cinterop.dconf.g_variant_new_boolean +import com.russhwolf.settings.cinterop.dconf.g_variant_new_double +import com.russhwolf.settings.cinterop.dconf.g_variant_new_int32 +import com.russhwolf.settings.cinterop.dconf.g_variant_new_int64 +import com.russhwolf.settings.cinterop.dconf.g_variant_new_string +import com.russhwolf.settings.cinterop.dconf.gintVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocPointerTo +import kotlinx.cinterop.get +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.toKString +import platform.posix.NULL + + +@OptIn(ExperimentalForeignApi::class) +@ExperimentalSettingsImplementation +public class DConfSettings(private val dir: String) : Settings { + // TODO sanitize slashes on `dir` + public class Factory(private val rootDir: String) : Settings.Factory { + // TODO sanitize slashes on `rootDir` and `name` + override fun create(name: String?): Settings { + val dir = if (name != null) "$rootDir/$name/" else "$rootDir/" + return DConfSettings("/${dir.removePrefix("/")}") + } + } + + override val keys: Set + get() = dConfOperation { dConfClient -> + buildSet { forEachKey(dConfClient) { add(it) } } + } + + override val size: Int + get() = dConfOperation { dConfClient -> + foldKeys(dConfClient, 0) { size, _ -> size + 1 } ?: 0 + } + + override fun clear(): Unit = dConfOperation { dConfClient -> + // TODO can we do this without repeating remove internals? (nested dConfOperation causes problems) + forEachKey(dConfClient) { key -> + val error = allocPointerTo() + val out = dconf_client_write_sync(dConfClient, "$dir$key", NULL?.reinterpret(), null, null, error.ptr) + if (out == FALSE) { + checkError(error.pointed) + } + } + } + + override fun remove(key: String) { + removeGVariant(key) + } + + override fun hasKey(key: String): Boolean = dConfOperation { dConfClient -> + forEachKey(dConfClient) { if (it == key) return@dConfOperation true } + false + } + + override fun putInt(key: String, value: Int): Unit = writeGVariant(key, value) + override fun getInt(key: String, defaultValue: Int): Int = getIntOrNull(key) ?: defaultValue + override fun getIntOrNull(key: String): Int? = readGVariant(key)?.let { g_variant_get_int32(it) } + + + override fun putLong(key: String, value: Long): Unit = writeGVariant(key, value) + override fun getLong(key: String, defaultValue: Long): Long = getLongOrNull(key) ?: defaultValue + override fun getLongOrNull(key: String): Long? = readGVariant(key)?.let { g_variant_get_int64(it) } + + override fun putString(key: String, value: String): Unit = writeGVariant(key, value) + override fun getString(key: String, defaultValue: String): String = getStringOrNull(key) ?: defaultValue + override fun getStringOrNull(key: String): String? = + readGVariant(key)?.let { g_variant_get_string(it, null)?.toKString() } + + override fun putFloat(key: String, value: Float): Unit = writeGVariant(key, value) + override fun getFloat(key: String, defaultValue: Float): Float = getFloatOrNull(key) ?: defaultValue + override fun getFloatOrNull(key: String): Float? = readGVariant(key)?.let { g_variant_get_double(it).toFloat() } + + override fun putDouble(key: String, value: Double): Unit = writeGVariant(key, value) + override fun getDouble(key: String, defaultValue: Double): Double = getDoubleOrNull(key) ?: defaultValue + override fun getDoubleOrNull(key: String): Double? = readGVariant(key)?.let { g_variant_get_double(it) } + + override fun putBoolean(key: String, value: Boolean): Unit = writeGVariant(key, value) + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = getBooleanOrNull(key) ?: defaultValue + override fun getBooleanOrNull(key: String): Boolean? = readGVariant(key)?.let { g_variant_get_boolean(it) == 1 } + + private inline fun MemScope.forEachKey(dConfClient: CPointer?, block: MemScope.(key: String) -> Unit) { + val lengthVar = alloc() + val list = dconf_client_list(dConfClient, dir, lengthVar.ptr) ?: return + + var index = 0 + var item = list[index] + while (item != null) { + val key = item.toKString() + if (dconf_is_rel_key(key, null) != FALSE) { + block(key) + } + + item = list[++index] + } + } + + private inline fun MemScope.foldKeys( + dConfClient: CPointer?, + initial: A, + block: MemScope.(accumulator: A, key: String) -> A + ): A { + var accumulator = initial + forEachKey(dConfClient) { accumulator = block(accumulator, it) } + return accumulator + } + + internal fun readGVariant(key: String): CPointer? = dConfOperation { dConfClient -> + dconf_client_read(dConfClient, "$dir$key") + } + + internal fun writeGVariant(key: String, value: T) = dConfOperation { dConfClient -> + val gVariant = gVariantOf(value) + val error = allocPointerTo() + val out = dconf_client_write_sync(dConfClient, "$dir$key", gVariant, null, null, error.ptr) + if (out == FALSE) { + checkError(error.pointed) + } + } + + internal fun removeGVariant(key: String) = writeGVariant(key, null) + + internal fun gVariantOf(value: T): CPointer? { + return when (value) { + null -> NULL?.reinterpret() + is Int -> g_variant_new_int32(value) + is Long -> g_variant_new_int64(value) + is String -> g_variant_new_string(value) + is Float -> g_variant_new_double(value.toDouble()) + is Double -> g_variant_new_double(value) + is Boolean -> g_variant_new_boolean(if (value) 1 else 0) + else -> error("Invalid value type for gVariant! value=$value") + } + } + + internal inline fun dConfOperation(action: MemScope.(dConfClient: CPointer?) -> T): T = memScoped { + val dConfClient = dconf_client_new() + g_object_ref(dConfClient) + val out = action(dConfClient) + dconf_client_sync(dConfClient) + g_object_unref(dConfClient) + out + } + + private fun checkError(error: GError?) { + if (error != null) { + error("dconf error: ${error.message?.toKString()}") + } + } +} diff --git a/multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt b/multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt new file mode 100644 index 00000000..af911e80 --- /dev/null +++ b/multiplatform-settings/src/linuxX64Test/kotlin/com/russhwolf/settings/DConfSettingsTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2024 Russell Wolf + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalForeignApi::class) + +package com.russhwolf.settings + +import com.russhwolf.settings.cinterop.dconf.GError +import com.russhwolf.settings.cinterop.dconf.TRUE +import com.russhwolf.settings.cinterop.dconf.dconf_client_list +import com.russhwolf.settings.cinterop.dconf.dconf_client_new +import com.russhwolf.settings.cinterop.dconf.dconf_client_read +import com.russhwolf.settings.cinterop.dconf.dconf_client_sync +import com.russhwolf.settings.cinterop.dconf.dconf_client_write_sync +import com.russhwolf.settings.cinterop.dconf.g_object_ref +import com.russhwolf.settings.cinterop.dconf.g_object_unref +import com.russhwolf.settings.cinterop.dconf.g_variant_get_int32 +import com.russhwolf.settings.cinterop.dconf.g_variant_new_int32 +import com.russhwolf.settings.cinterop.dconf.gintVar +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocPointerTo +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.value +import platform.posix.NULL +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalSettingsImplementation::class) +public class DConfSettingsTest : BaseSettingsTest( + platformFactory = DConfSettings.Factory("com/russhwolf/settings/test"), + hasListeners = false +) { + @Test + fun foo(): Unit = memScoped { + val dConfClient = dconf_client_new() + g_object_ref(dConfClient) + + try { + val error = allocPointerTo() + val writeStatus = dconf_client_write_sync( + client = dConfClient, + key = "/com/russhwolf/settings/test/foo", + value = g_variant_new_int32(42), + tag = NULL?.reinterpret(), + cancellable = NULL?.reinterpret(), + error = error.ptr + ) + + assertEquals(TRUE, writeStatus) + + dconf_client_sync(dConfClient) + + val keysSizeVar = alloc() + val keys = dconf_client_list(dConfClient, "/com/russhwolf/settings/test/", keysSizeVar.ptr) + val keysSize = keysSizeVar.value + assertTrue(keysSize > 0) + val read = dconf_client_read(dConfClient, "/com/russhwolf/settings/test/foo") + assertNotNull(read) + assertEquals(42, g_variant_get_int32(read)) + } finally { + dconf_client_sync(dConfClient) + g_object_unref(dConfClient) + } + + } + // TODO add test cases to verify that we write to the files we think we do + + // TODO add cleanup methods so we don't leave test DBs lying around +} diff --git a/multiplatform-settings/src/nativeInterop/cinterop/dconf.def b/multiplatform-settings/src/nativeInterop/cinterop/dconf.def new file mode 100644 index 00000000..2854a194 --- /dev/null +++ b/multiplatform-settings/src/nativeInterop/cinterop/dconf.def @@ -0,0 +1,4 @@ +headers = dconf.h +package = com.russhwolf.settings.cinterop.dconf +compilerOpts = -I/usr/include/dconf -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include +linkerOpts = -ldconf -lglib-2.0 -lgobject-2.0 -L/usr/lib -L/usr/lib/x86_64-linux-gnu/