From 21738818d571f0a8726cfa255682d8781325d77e Mon Sep 17 00:00:00 2001 From: mae Date: Sat, 4 Oct 2025 22:11:40 -0500 Subject: [PATCH] write hakurei dsl --- .../moe/rosa/planterette/PlanteretteConfig.kt | 5 + .../kotlin/moe/rosa/planterette/dsl/DSL.kt | 12 + .../moe/rosa/planterette/dsl/HakureiDSL.kt | 392 ++++++++++++++++++ .../moe/rosa/planterette/hakurei/Hakurei.kt | 86 ++-- src/test/kotlin/DSLTest.kt | 111 +++++ src/test/kotlin/HakureiTest.kt | 5 +- 6 files changed, 564 insertions(+), 47 deletions(-) create mode 100644 src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt create mode 100644 src/main/kotlin/moe/rosa/planterette/dsl/DSL.kt create mode 100644 src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt create mode 100644 src/test/kotlin/DSLTest.kt diff --git a/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt b/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt new file mode 100644 index 0000000..709c92b --- /dev/null +++ b/src/main/kotlin/moe/rosa/planterette/PlanteretteConfig.kt @@ -0,0 +1,5 @@ +package moe.rosa.planterette + +import moe.rosa.planterette.hakurei.HakureiConfig + +data class PlanteretteConfig(var hakurei: HakureiConfig?) \ No newline at end of file diff --git a/src/main/kotlin/moe/rosa/planterette/dsl/DSL.kt b/src/main/kotlin/moe/rosa/planterette/dsl/DSL.kt new file mode 100644 index 0000000..9724e48 --- /dev/null +++ b/src/main/kotlin/moe/rosa/planterette/dsl/DSL.kt @@ -0,0 +1,12 @@ +package moe.rosa.planterette.dsl + +import moe.rosa.planterette.PlanteretteConfig + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@DslMarker +annotation class PlanteretteDSL + +@PlanteretteDSL +fun planterette(init: PlanteretteConfig.() -> Unit): PlanteretteConfig { + return PlanteretteConfig(hakurei = null).apply(init) +} \ No newline at end of file diff --git a/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt b/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt new file mode 100644 index 0000000..01f50ff --- /dev/null +++ b/src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt @@ -0,0 +1,392 @@ +package moe.rosa.planterette.dsl + +import moe.rosa.planterette.PlanteretteConfig +import moe.rosa.planterette.dsl.DSLEnablements.* +import moe.rosa.planterette.hakurei.* + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@PlanteretteDSL +annotation class HakureiDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@HakureiDSL +annotation class DBusDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@HakureiDSL +annotation class ExtraPermsDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@HakureiDSL +annotation class ContainerDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@ContainerDSL +annotation class FilesystemDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@FilesystemDSL +annotation class FSBindDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@FilesystemDSL +annotation class FSEphemeralDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@FilesystemDSL +annotation class FSLinkDSL + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +@FilesystemDSL +annotation class FSOverlayDSL + +@PlanteretteDSL +fun PlanteretteConfig.hakurei(id: String, init: @HakureiDSL HakureiConfig.() -> Unit) { + this.hakurei = HakureiConfig(id).apply(init) +} +@HakureiDSL +fun HakureiConfig.executable(path: String, vararg args: String) { + this.path = AbsolutePath(path) + this.args = args.toList() +} +@HakureiDSL +enum class DSLEnablements { + Wayland, + X11, + DBus, + Pulse +} +@HakureiDSL +fun HakureiConfig.enable(vararg enablements: DSLEnablements) { + val enable = Enablements(wayland = null, x11 = null, dbus = null, pulse = null) + enablements.map { + when(it) { + Wayland -> enable.wayland = true + X11 -> enable.x11 = true + DBus -> enable.dbus = true + Pulse -> enable.pulse = true + } + } + this.enablements = enable +} +@HakureiDSL +fun HakureiConfig.directWayland(directWayland: Boolean = true) { + this.directWayland = directWayland +} +@HakureiDSL +fun HakureiConfig.username(username: String) { + this.username = username +} +@HakureiDSL +fun HakureiConfig.shell(shell: String) { + this.shell = AbsolutePath(shell) +} +@HakureiDSL +fun HakureiConfig.home(home: String) { + this.home = AbsolutePath(home) +} +//TODO(mae) automatic identity? +@HakureiDSL +fun HakureiConfig.identity(identity: Int? = null) { + this.identity = identity +} +@HakureiDSL +fun HakureiConfig.groups(vararg groups: String) { + this.groups = groups.toList() +} +data class DBusConfigs(var session: DBusConfig? = null, var system: DBusConfig? = null) + +@HakureiDSL +fun HakureiConfig.dbus(init: @DBusDSL DBusConfigs.() -> Unit) { + val dbus = DBusConfigs().apply(init) + this.sessionBus = dbus.session + this.systemBus = dbus.system +} +@DBusDSL +fun DBusConfigs.session(init: @DBusDSL DBusConfig.() -> Unit) { + this.session = DBusConfig().apply(init) +} +@DBusDSL +fun DBusConfigs.system(init: @DBusDSL DBusConfig.() -> Unit) { + this.system = DBusConfig().apply(init) +} +@DBusDSL +fun DBusConfig.see(vararg see: String) { + this.see = see.toList() +} +@DBusDSL +fun DBusConfig.talk(vararg talk: String) { + this.talk = talk.toList() +} +@DBusDSL +fun DBusConfig.own(vararg own: String) { + this.own = own.toList() +} +@DBusDSL +fun DBusConfig.call(vararg call: Pair) { + this.call = call.toMap() +} +@DBusDSL +fun DBusConfig.broadcast(vararg broadcast: Pair) { + this.broadcast = broadcast.toMap() +} +@DBusDSL +fun DBusConfig.log(log: Boolean = true) { + this.log = log +} +@DBusDSL +fun DBusConfig.filter(filter: Boolean = true) { + this.filter = filter +} +@HakureiDSL +fun HakureiConfig.extraPerms(vararg extraPerms: ExtraPermsConfig) { + this.extraPerms = extraPerms.toList() +} +@ExtraPermsDSL +fun perm(path: String, init: ExtraPermsConfig.() -> Unit): ExtraPermsConfig { + return ExtraPermsConfig(path = AbsolutePath(path)).apply(init) +} +@ExtraPermsDSL +fun perm(path: String, ensure: Boolean? = null, rwx: String): ExtraPermsConfig { + if(rwx.length != 3) throw IllegalArgumentException() + // TODO(mae): is there a difference between null and false in this case? + val read: Boolean? = when(rwx[0]) { + 'r', 'R' -> true + else -> null + } + val write: Boolean? = when(rwx[1]) { + 'w', 'W' -> true + else -> null + } + val execute: Boolean? = when(rwx[2]) { + 'x', 'X' -> true + else -> null + } + return ExtraPermsConfig(ensure, path = AbsolutePath(path), read, write, execute) +} +@ExtraPermsDSL +fun ExtraPermsConfig.ensure(ensure: Boolean = true) { + this.ensure = ensure +} +@ExtraPermsDSL +fun ExtraPermsConfig.read(read: Boolean = true) { + this.read = read +} +@ExtraPermsDSL +fun ExtraPermsConfig.write(write: Boolean = true) { + this.write = write +} +@ExtraPermsDSL +fun ExtraPermsConfig.execute(execute: Boolean = true) { + this.execute = execute +} + +@HakureiDSL +fun HakureiConfig.container(init: @ContainerDSL ContainerConfig.() -> Unit) { + this.container = ContainerConfig().apply(init) +} +@ContainerDSL +fun ContainerConfig.hostname(hostname: String) { + this.hostname = hostname +} +@ContainerDSL +fun ContainerConfig.waitDelay(waitDelay: Long) { + this.waitDelay = waitDelay +} +@ContainerDSL +fun ContainerConfig.noTimeout() { + this.waitDelay = -1 +} +@ContainerDSL +fun ContainerConfig.seccompCompat(seccompCompat: Boolean = true) { + this.seccompCompat = seccompCompat +} +@ContainerDSL +fun ContainerConfig.devel(devel: Boolean = true) { + this.devel = devel +} +@ContainerDSL +fun ContainerConfig.userns(userns: Boolean = true) { + this.userns = userns +} +@ContainerDSL +fun ContainerConfig.hostNet(hostNet: Boolean = true) { + this.hostNet = hostNet +} +@ContainerDSL +fun ContainerConfig.hostAbstract(hostAbstract: Boolean = true) { + this.hostAbstract = hostAbstract +} +@ContainerDSL +fun ContainerConfig.tty(tty: Boolean = true) { + this.tty = tty +} +@ContainerDSL +fun ContainerConfig.multiarch(multiarch: Boolean = true) { + this.multiarch = multiarch +} +@ContainerDSL +fun ContainerConfig.env(vararg env: Pair) { + this.env = env.toMap() +} +@ContainerDSL +fun ContainerConfig.mapRealUid(mapRealUid: Boolean = true) { + this.mapRealUid = mapRealUid +} +@ContainerDSL +fun ContainerConfig.device(device: Boolean = true) { + this.device = device +} + +@FilesystemDSL +data class FilesystemConfigs(val configs: MutableList = mutableListOf()) + +@ContainerDSL +fun ContainerConfig.filesystem(init: @FilesystemDSL FilesystemConfigs.() -> Unit) { + val config = FilesystemConfigs().apply(init) + this.filesystem = config.configs +} +@FilesystemDSL +data class DummyFSBind(var target: String? = null, + var source: String? = null, + var write: Boolean? = null, + var device: Boolean? = null, + var ensure: Boolean? = null, + var optional: Boolean? = null, + var special: Boolean? = null) { + fun build(): FSBind { + return FSBind( + target = if(target != null) { AbsolutePath(target!!) } else null, + source = AbsolutePath(source!!), + write = write, + device = device, + ensure = ensure, + optional = optional, + special = special + ) + } +} +@FilesystemDSL +fun FilesystemConfigs.bind(src2dst: Pair, init: @FSBindDSL DummyFSBind.() -> Unit = {}) { + val fs = DummyFSBind(target = src2dst.second, source = src2dst.first) + fs.apply(init) + this.configs.add(fs.build()) +} +@FilesystemDSL +fun FilesystemConfigs.bind(source: String, init: @FSBindDSL DummyFSBind.() -> Unit = {}) { + val fs = DummyFSBind(source = source) + fs.apply(init) + this.configs.add(fs.build()) +} +@FSBindDSL +fun DummyFSBind.write(write: Boolean? = true) { + this.write = write +} +@FSBindDSL +fun DummyFSBind.device(device: Boolean? = true) { + this.device = device +} +@FSBindDSL +fun DummyFSBind.ensure(ensure: Boolean? = true) { + this.ensure = ensure +} +@FSBindDSL +fun DummyFSBind.optional(optional: Boolean? = true) { + this.optional = optional +} +@FSBindDSL +fun DummyFSBind.special(special: Boolean? = true) { + this.special = special +} +@FilesystemDSL +data class DummyFSEphemeral(val target: String? = null, + var write: Boolean? = null, + var size: Int? = null, + var perm: Int? = null) { + fun build(): FSEphemeral { + return FSEphemeral( + target = AbsolutePath(target!!), + write = write!!, + size = size, + perm = perm!! + ) + } +} +@FSEphemeralDSL +fun DummyFSEphemeral.write(write: Boolean = true) { + this.write = write +} +@FSEphemeralDSL +fun DummyFSEphemeral.size(size: Int) { + this.size = size +} +@FSEphemeralDSL +fun DummyFSEphemeral.perm(perm: Int) { + this.perm = perm +} +@FilesystemDSL +fun FilesystemConfigs.ephemeral(target: String, init: @FSEphemeralDSL DummyFSEphemeral.() -> Unit = {}) { + val fs = DummyFSEphemeral(target = target) + fs.apply(init) + this.configs.add(fs.build()) +} +@FilesystemDSL +data class DummyFSLink(val target: String? = null, + val linkname: String? = null, + var dereference: Boolean? = null) { + fun build(): FSLink { + return FSLink( + target = AbsolutePath(target!!), + linkname = linkname!!, + dereference = dereference!! + ) + } +} +@FSLinkDSL +fun DummyFSLink.dereference(dereference: Boolean = true) { + this.dereference = dereference +} +@FilesystemDSL +fun FilesystemConfigs.link(lnk2dst: Pair, init: @FSLinkDSL DummyFSLink.() -> Unit = {}) { + val fs = DummyFSLink(target = lnk2dst.second, linkname = lnk2dst.first) + fs.apply(init) + this.configs.add(fs.build()) +} +@FilesystemDSL +fun FilesystemConfigs.link(target: String, init: @FSLinkDSL DummyFSLink.() -> Unit = {}) { + val fs = DummyFSLink(target = target, linkname = target) + fs.apply(init) + this.configs.add(fs.build()) +} +@FilesystemDSL +data class DummyFSOverlay(val target: String? = null, + var lower: MutableList? = mutableListOf(), + var upper: String? = null, + var work: String? = null) { + fun build(): FSOverlay { + return FSOverlay( + target = AbsolutePath(target!!), + lower = lower!!.map { AbsolutePath(it)}, + upper = AbsolutePath(upper!!), + work = AbsolutePath(work!!) + ) + } +} +@FilesystemDSL +fun FilesystemConfigs.overlay(target: String, init: @FSOverlayDSL DummyFSOverlay.() -> Unit = {}) { + val fs = DummyFSOverlay(target = target) + fs.apply(init) + this.configs.add(fs.build()) +} +@FSOverlayDSL +fun DummyFSOverlay.lower(vararg lower: String) { + this.lower!!.addAll(lower.toList()) +} +@FSOverlayDSL +fun DummyFSOverlay.upper(upper: String) { + this.upper = upper +} +@FSOverlayDSL +fun DummyFSOverlay.work(work: String) { + this.work = work +} \ No newline at end of file diff --git a/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt b/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt index 3d761d8..b3528d1 100644 --- a/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt +++ b/src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt @@ -4,51 +4,51 @@ import kotlinx.serialization.* @Serializable data class HakureiConfig( - val id: String? = null, - val path: AbsolutePath? = null, - val args: List? = null, - val enablements: Enablements? = null, - @SerialName("session_bus") val sessionBus: DBusConfig? = null, - @SerialName("system_bus") val systemBus: DBusConfig? = null, - @SerialName("direct_wayland") val directWayland: Boolean? = null, - val username: String? = null, - val shell: AbsolutePath? = null, - val home: AbsolutePath? = null, + var id: String? = null, + var path: AbsolutePath? = null, + var args: List? = null, + var enablements: Enablements? = null, + @SerialName("session_bus") var sessionBus: DBusConfig? = null, + @SerialName("system_bus") var systemBus: DBusConfig? = null, + @SerialName("direct_wayland") var directWayland: Boolean? = null, + var username: String? = null, + var shell: AbsolutePath? = null, + var home: AbsolutePath? = null, - @SerialName("extra_perms") val extraPerms: List? = null, - val identity: Int? = null, - val groups: List? = null, + @SerialName("extra_perms") var extraPerms: List? = null, + var identity: Int? = null, + var groups: List? = null, - val container: ContainerConfig? = null, + var container: ContainerConfig? = null, ) @Serializable data class ContainerConfig( - val hostname: String? = null, - @SerialName("wait_delay") val waitDelay: Long? = null, - @SerialName("seccomp_compat") val seccompCompat: Boolean? = null, - val devel: Boolean? = null, - val userns: Boolean? = null, - @SerialName("host_net") val hostNet: Boolean? = null, - @SerialName("host_abstract") val hostAbstract: Boolean? = null, - val tty: Boolean? = null, - val multiarch: Boolean? = null, + var hostname: String? = null, + @SerialName("wait_delay") var waitDelay: Long? = null, + @SerialName("seccomp_compat") var seccompCompat: Boolean? = null, + var devel: Boolean? = null, + var userns: Boolean? = null, + @SerialName("host_net") var hostNet: Boolean? = null, + @SerialName("host_abstract") var hostAbstract: Boolean? = null, + var tty: Boolean? = null, + var multiarch: Boolean? = null, - val env: Map? = null, + var env: Map? = null, - @SerialName("map_real_uid") val mapRealUid: Boolean? = null, - val device: Boolean? = null, + @SerialName("map_real_uid") var mapRealUid: Boolean? = null, + var device: Boolean? = null, - val filesystem: List? = null, + var filesystem: List? = null, ) @Serializable data class ExtraPermsConfig( - val ensure: Boolean? = null, - val path: AbsolutePath, - @SerialName("r") val read: Boolean? = null, - @SerialName("w") val write: Boolean? = null, - @SerialName("x") val execute: Boolean? = null, + var ensure: Boolean? = null, + var path: AbsolutePath, + @SerialName("r") var read: Boolean? = null, + @SerialName("w") var write: Boolean? = null, + @SerialName("x") var execute: Boolean? = null, ) { override fun toString(): String { val buffer = StringBuffer(5 + path.toString().length) @@ -73,19 +73,19 @@ data class ExtraPermsConfig( @Serializable data class DBusConfig( - val see: List? = null, - val talk: List? = null, - val own: List? = null, - val call: Map? = null, - val broadcast: Map? = null, - val log: Boolean? = null, - val filter: Boolean? = null, + var see: List? = null, + var talk: List? = null, + var own: List? = null, + var call: Map? = null, + var broadcast: Map? = null, + var log: Boolean? = null, + var filter: Boolean? = null, ) @Serializable data class Enablements( - val wayland: Boolean? = null, - val x11: Boolean? = null, - val dbus: Boolean? = null, - val pulse: Boolean? = null, + var wayland: Boolean? = null, + var x11: Boolean? = null, + var dbus: Boolean? = null, + var pulse: Boolean? = null, ) diff --git a/src/test/kotlin/DSLTest.kt b/src/test/kotlin/DSLTest.kt new file mode 100644 index 0000000..0044dab --- /dev/null +++ b/src/test/kotlin/DSLTest.kt @@ -0,0 +1,111 @@ +import moe.rosa.planterette.dsl.* +import moe.rosa.planterette.dsl.DSLEnablements.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class DSLTest { + companion object { + val HAKUREI_DSL_TEST = planterette { + hakurei("org.chromium.Chromium") { + executable("/run/current-system/sw/bin/chromium", + "chromium", + "--ignore-gpu-blocklist", + "--disable-smooth-scrolling", + "--enable-features=UseOzonePlatform", + "--ozone-platform=wayland" + ) + enable(Wayland, DBus, Pulse) + dbus { + session { + talk("org.freedesktop.Notifications", + "org.freedesktop.FileManager1", + "org.freedesktop.ScreenSaver", + "org.freedesktop.secrets", + "org.kde.kwalletd5", + "org.kde.kwalletd6", + "org.gnome.SessionManager") + own("org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.org.chromium.Chromium.*", + "org.mpris.MediaPlayer2.chromium.*") + call("org.freedesktop.portal.*" to "*") + broadcast("org.freedesktop.portal.*" to "@/org/freedesktop/portal/*") + filter() + } + system { + talk("org.bluez", + "org.freedesktop.Avahi", + "org.freedesktop.UPower") + filter() + } + } + username("chronos") + shell("/run/current-system/sw/bin/zsh") + home("/data/data/org.chromium.Chromium") + extraPerms( + perm("/var/lib/hakurei/u0") { + ensure() + execute() + }, + perm("/var/lib/hakurei/u0/org.chromium.Chromium", rwx = "rwx") + ) + identity(9) + groups("video", + "dialout", + "plugdev") + container { + hostname("localhost") + noTimeout() + seccompCompat() + devel() + userns() + hostNet() + hostAbstract() + tty() + multiarch() + env("GOOGLE_API_KEY" to "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY", + "GOOGLE_DEFAULT_CLIENT_ID" to "77185425430.apps.googleusercontent.com", + "GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT") + mapRealUid() + device() + filesystem { + bind("/var/lib/hakurei/base/org.debian" to "/") { + write() + special() + } + bind("/etc/" to "/etc/") { + special() + } + ephemeral("/tmp/") { + write() + perm(493) + } + overlay("/nix/store") { + lower("/mnt-root/nix/.ro-store") + upper("/mnt-root/nix/.rw-store/upper") + work("/mnt-root/nix/.rw-store/work") + } + bind("/nix/store") + link("/run/current-system") { + dereference() + } + link("/run/opengl-driver") { + dereference() + } + bind("/var/lib/hakurei/u0/org.chromium.Chromium" to "/data/data/org.chromium.Chromium") { + write() + ensure() + } + bind("/dev/dri") { + device() + optional() + } + } + } + } + } + } + @Test + fun hakureiDSLTest() { + assertEquals(HakureiTest.TEMPLATE_DATA, HAKUREI_DSL_TEST.hakurei) + } +} diff --git a/src/test/kotlin/HakureiTest.kt b/src/test/kotlin/HakureiTest.kt index 3c068f6..ad5537f 100644 --- a/src/test/kotlin/HakureiTest.kt +++ b/src/test/kotlin/HakureiTest.kt @@ -2,10 +2,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import moe.rosa.planterette.hakurei.* import org.junit.jupiter.api.assertDoesNotThrow -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertIs +import kotlin.test.* class HakureiTest { companion object {