Compare commits

..

5 Commits

Author SHA1 Message Date
mae
aa3c3377d0 insert native lib into jar 2025-11-16 20:34:38 -06:00
mae
96bb989ad5 write to fd 2025-11-16 15:09:29 -06:00
mae
98950f21f3 write partial kotlin dsl 2025-11-15 21:53:36 -06:00
mae
7110fdb53e delete outdated kotlin source 2025-11-15 19:20:30 -06:00
mae
d52447dc23 write go manifest representation 2025-11-15 19:18:46 -06:00
18 changed files with 546 additions and 1064 deletions

79
api/capability.go Normal file
View File

@@ -0,0 +1,79 @@
package api
import (
"encoding/json"
"fmt"
)
type Capability interface {
ID() string
}
type CapabilityJSON struct {
Capability
}
type BasicCapability struct {
Id string `json:"id"`
}
func (c BasicCapability) ID() string {
return c.Id
}
const CapabilityBasic string = "basic"
type DBusCapability struct {
Id string `json:"id"`
Own []string `json:"own,omitempty"`
}
func (c DBusCapability) ID() string {
return c.Id
}
const CapabilityDBus = "dbus"
type capabilityType struct {
Type string `json:"type"`
}
func (c *CapabilityJSON) MarshalJSON() ([]byte, error) {
if c == nil || c.Capability == nil {
return nil, fmt.Errorf("invalid capability")
}
var v any
switch cv := c.Capability.(type) {
case *BasicCapability:
v = &struct {
capabilityType
*BasicCapability
}{capabilityType{CapabilityBasic}, cv}
case *DBusCapability:
v = &struct {
capabilityType
*DBusCapability
}{capabilityType{CapabilityDBus}, cv}
default:
return nil, fmt.Errorf("invalid capability")
}
return json.Marshal(v)
}
func (c *CapabilityJSON) UnmarshalJSON(data []byte) error {
t := new(capabilityType)
if err := json.Unmarshal(data, &t); err != nil {
return err
}
switch t.Type {
case CapabilityBasic:
*c = CapabilityJSON{new(BasicCapability)}
case CapabilityDBus:
*c = CapabilityJSON{new(DBusCapability)}
default:
return fmt.Errorf("invalid capability")
}
return json.Unmarshal(data, c.Capability)
}

25
api/manifest.go Normal file
View File

@@ -0,0 +1,25 @@
package api
import "hakurei.app/container/check"
type Manifest struct {
Metadata Metadata `json:"metadata"`
Executable Executable `json:"executable"`
Capabilities []CapabilityJSON `json:"capabilities"`
Permissions []CapabilityJSON `json:"permissions"`
}
type Metadata struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Author string `json:"author"`
Icon *check.Absolute `json:"icon"`
Version int `json:"version"`
VersionName string `json:"version_name"`
}
type Executable struct {
BaseImage string `json:"base_image"`
Binary *check.Absolute `json:"binary"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
}

View File

@@ -10,6 +10,10 @@ repositories {
mavenCentral()
}
val kotlinVersion = "2.2.10"
val kotlinCoroutinesVersion = "1.7.0-RC"
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -18,8 +22,18 @@ dependencies {
testImplementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation(kotlin("reflect"))
implementation("org.jetbrains.kotlin:kotlin-scripting-common:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-scripting-jvm:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies-maven:${kotlinVersion}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${kotlinCoroutinesVersion}")
}
sourceSets.main {
resources {
srcDirs("src/main/resources", "build/natives")
}
}
kotlin {
jvmToolchain(24)
}
@@ -27,4 +41,12 @@ kotlin {
tasks.test {
useJUnitPlatform()
}
project.layout.buildDirectory.set(file("../build"))
tasks.register<Exec>("compileNativeLib") {
val java_home = System.getProperty("java.home")
environment("CGO_CFLAGS" to "-I$java_home/include -I$java_home/include/linux")
workingDir("src/main/go")
commandLine("go", "build", "-linkshared", "-buildmode=c-shared", "-o", layout.buildDirectory.get().dir("natives").file("pltbuild.so"))
}
tasks.processResources {
dependsOn += "compileNativeLib"
}

View File

@@ -0,0 +1,37 @@
package main
//#include "pltbuild.h"
import "C"
import (
"errors"
"os"
"strconv"
"syscall"
)
//export planterette_write
func planterette_write(
fd C.int,
str_p *C.char, str_sz C.size_t,
errno_p *C.uintptr_t,
err_str_p **C.char,
) {
f := os.NewFile(uintptr(fd), strconv.Itoa(int(fd)))
defer f.Close()
_, err := f.WriteString(C.GoStringN(str_p, C.int(str_sz)))
if err == nil {
return
}
var pathError *os.PathError
if errors.As(err, &pathError) {
var errno syscall.Errno
if errors.As(pathError.Err, &errno) {
*errno_p = C.uintptr_t(errno)
return
}
}
*err_str_p = C.CString(err.Error())
return
}

View File

@@ -0,0 +1,26 @@
#include "pltbuild.h"
jint throwIOException( JNIEnv *env, char *message) {
jclass exClass;
char *className = "java/io/IOException";
exClass = (*env)->FindClass(env, className);
return (*env)->ThrowNew(env, exClass, message);
}
JNIEXPORT void JNICALL Java_moe_rosa_planterette_jni_GoFile_write__ILnet_java_String_2(JNIEnv *env, jobject obj, jint fd, jstring str) {
char *s = (*env)->GetStringUTFChars(env, str, 0);
size_t sz = (*env)->GetStringUTFLength(env, str);
uintptr_t *errno_p = NULL;
char **err_str_p = NULL;
planterette_write(fd, s, sz, errno_p, err_str_p);
if(errno_p || *err_str_p ) {
throwIOException(env, **err_str_p ? *err_str_p : strerror(*errno_p));
}
(*env)->ReleaseStringUTFChars(env, str, s);
free(err_str_p);
}

View File

@@ -0,0 +1,17 @@
#include <jni.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#ifndef _PLTBUILD_H
#define _PLTBUILD_H
/*
* moe/rosa/planterette/jni/GoFile#write(ILnet/java/String;)V
*/
JNIEXPORT void JNICALL Java_moe_rosa_planterette_jni_GoFile_write__ILnet_java_String_2(JNIEnv *, jobject, jint, jstring);
extern void planterette_write(int fd, char *str_p, size_t str_sz, uintptr_t *errno_p, char **err_str_p);
#endif

View File

@@ -1,9 +0,0 @@
package moe.rosa.planterette
import moe.rosa.planterette.hakurei.HakureiConfig
/**
* Represents a Planterette build configuration.
* @param hakurei Hakurei container configuration for the application.
*/
data class PlanteretteConfig(var hakurei: HakureiConfig?)

View File

@@ -0,0 +1,16 @@
package moe.rosa.planterette.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
sealed interface Capability {
val id: String
@SerialName("basic")
data class Basic(override val id: String) : Capability
@SerialName("dbus")
data class DBus(override val id: String, val own: List<String>) : Capability
}

View File

@@ -0,0 +1,36 @@
package moe.rosa.planterette.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Manifest(
val metadata: Metadata,
val executable: Executable,
val capabilities: List<Capability>,
val permissions: List<Capability>
) {
@Serializable
data class Metadata(
var id: String,
var name: String,
var description: String,
var author: String,
var icon: String,
var version: Int,
@SerialName("version_name") var versionName: String
) {
constructor() : this("", "", "", "", "", -1, "")
}
@Serializable
data class Executable(
@SerialName("base_image") var baseImage: String,
var binary: String,
val args: MutableList<String>,
val env: MutableMap<String, String>
) {
constructor() : this("", "", mutableListOf(), mutableMapOf())
}
}

View File

@@ -1,12 +1,227 @@
@file:Suppress("unused")
package moe.rosa.planterette.dsl
import moe.rosa.planterette.PlanteretteConfig
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import moe.rosa.planterette.api.Capability
import moe.rosa.planterette.api.Manifest
import moe.rosa.planterette.jni.GoFile
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@DslMarker
annotation class PlanteretteDSL
annotation class PlanteretteBlockMarker
@PlanteretteDSL
fun planterette(init: PlanteretteConfig.() -> Unit): PlanteretteConfig {
return PlanteretteConfig(hakurei = null).apply(init)
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PlanteretteBlockMarker
annotation class MetadataBlockMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PlanteretteBlockMarker
annotation class ExecutableBlockMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PlanteretteBlockMarker
annotation class CapabilitiesBlockMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@CapabilitiesBlockMarker
annotation class DBusBlockMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PlanteretteBlockMarker
annotation class PermissionsBlockMarker
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@PermissionsBlockMarker
annotation class FilesystemBlockMarker
@PlanteretteBlockMarker
data class PlanteretteBlock(
var metadata: MetadataBlock,
var executable: ExecutableBlock,
var capabilities: CapabilitiesBlock,
var permissions: PermissionsBlock,
var buildGuard: Boolean = false
) {
// NOTE the two underscores are because this function is only intended to be added at runtime
@OptIn(ExperimentalSerializationApi::class)
@Suppress("FunctionName")
fun __build(args: Array<out String>): PlanteretteBlock {
if(!buildGuard) {
val manifest = Manifest(metadata.metadata, executable.executable, capabilities.capabilities, permissions.permissions)
GoFile.write(args[0].toInt(), Json.encodeToString(manifest))
buildGuard = true
}
return this;
}
}
@MetadataBlockMarker
data class MetadataBlock(var metadata: Manifest.Metadata)
@ExecutableBlockMarker
data class ExecutableBlock(var executable: Manifest.Executable)
@CapabilitiesBlockMarker
data class CapabilitiesBlock(var capabilities: MutableList<Capability>)
// TODO add the rest of this
@DBusBlockMarker
data class DBusBlock(var own: MutableList<String>)
@PermissionsBlockMarker
data class PermissionsBlock(var permissions: MutableList<Capability>)
@FilesystemBlockMarker
class FilesystemBlock(var filesystemPermissions: MutableList<Capability>)
@PlanteretteBlockMarker
fun planterette(init: PlanteretteBlock.() -> Unit) {
PlanteretteBlock(MetadataBlock(Manifest.Metadata()), ExecutableBlock(Manifest.Executable()), CapabilitiesBlock(mutableListOf()), PermissionsBlock(mutableListOf())).apply(init)
}
@PlanteretteBlockMarker
fun PlanteretteBlock.metadata(init: @MetadataBlockMarker MetadataBlock.() -> Unit) {
this.metadata.apply(init)
}
@MetadataBlockMarker
fun MetadataBlock.id(id: String) {
this.metadata.id = id
}
@MetadataBlockMarker
fun MetadataBlock.name(name: String) {
this.metadata.name = name
}
@MetadataBlockMarker
fun MetadataBlock.description(description: String) {
this.metadata.description = description
}
@MetadataBlockMarker
fun MetadataBlock.author(author: String) {
this.metadata.author = author
}
@MetadataBlockMarker
fun MetadataBlock.icon(icon: String) {
this.metadata.icon = icon
}
@MetadataBlockMarker
fun MetadataBlock.version(number: Int, name: String) {
this.metadata.version = number
this.metadata.versionName = name
}
@PlanteretteBlockMarker
fun PlanteretteBlock.executable(init: @ExecutableBlockMarker ExecutableBlock.() -> Unit) {
this.executable.apply(init)
}
@ExecutableBlockMarker
fun ExecutableBlock.baseImage(baseImage: String) {
this.executable.baseImage = baseImage
}
@ExecutableBlockMarker
fun ExecutableBlock.binary(binary: String) {
this.executable.binary = binary
}
@ExecutableBlockMarker
fun ExecutableBlock.args(vararg args: String) {
this.executable.args.addAll(args)
}
@ExecutableBlockMarker
fun ExecutableBlock.env(vararg env: Pair<String, String>) {
this.executable.env.putAll(env)
}
@PlanteretteBlockMarker
fun PlanteretteBlock.capabilities(init: @CapabilitiesBlockMarker CapabilitiesBlock.() -> Unit) {
this.capabilities.apply(init)
}
internal fun CapabilitiesBlock.addBasic(id: String) {
this.capabilities.add(Capability.Basic(id))
}
@CapabilitiesBlockMarker
fun CapabilitiesBlock.portal() {
addBasic("moe.rosa.capabilities.Portal")
}
@CapabilitiesBlockMarker
fun CapabilitiesBlock.secrets() {
addBasic("moe.rosa.capabilities.Secrets")
}
@CapabilitiesBlockMarker
fun CapabilitiesBlock.developer() {
addBasic("moe.rosa.capabilities.Developer")
}
@CapabilitiesBlockMarker
fun CapabilitiesBlock.graphics() {
addBasic("moe.rosa.capabilities.Graphics")
}
@CapabilitiesBlockMarker
fun CapabilitiesBlock.audio() {
addBasic("moe.rosa.capabilities.Audio")
}
@CapabilitiesBlockMarker
fun CapabilitiesBlock.dbus(init: @DBusBlockMarker DBusBlock.() -> Unit) {
val dbus = DBusBlock(mutableListOf())
this.capabilities.add(Capability.DBus("moe.rosa.capabilities.DBus", dbus.own))
}
@PlanteretteBlockMarker
fun PlanteretteBlock.permissions(init: @PermissionsBlockMarker PermissionsBlock.() -> Unit) {
this.permissions.apply(init)
}
internal fun PermissionsBlock.addBasic(id: String) {
this.permissions.add(Capability.Basic(id))
}
@PermissionsBlockMarker
fun PermissionsBlock.notifications() {
addBasic("moe.rosa.permissions.Notifications")
}
@PermissionsBlockMarker
fun PermissionsBlock.filesystem(init: @FilesystemBlockMarker FilesystemBlock.() -> Unit) {
this.permissions.addAll(FilesystemBlock(mutableListOf()).apply(init).filesystemPermissions)
}
@FilesystemBlockMarker
fun FilesystemBlock.fileManager() {
this.filesystemPermissions.add(Capability.Basic("moe.rosa.permissions.filesystem.FileManager"))
}
@FilesystemBlockMarker
fun FilesystemBlock.etc() {
this.filesystemPermissions.add(Capability.Basic("moe.rosa.permissions.filesystem.Etc"))
}
@FilesystemBlockMarker
fun FilesystemBlock.tmp() {
this.filesystemPermissions.add(Capability.Basic("moe.rosa.permissions.filesystem.Tmp"))
}
@PermissionsBlockMarker
fun PermissionsBlock.network() {
addBasic("moe.rosa.permissions.Network")
}
@PermissionsBlockMarker
fun PermissionsBlock.system() {
addBasic("moe.rosa.permissions.System")
}

View File

@@ -1,393 +0,0 @@
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
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
}
//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<String, String>) {
this.call = call.toMap()
}
@DBusDSL
fun DBusConfig.broadcast(vararg broadcast: Pair<String, String>) {
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<String, String>) {
this.env = env.toMap()
}
@ContainerDSL
fun ContainerConfig.mapRealUid(mapRealUid: Boolean = true) {
this.mapRealUid = mapRealUid
}
@ContainerDSL
fun ContainerConfig.device(device: Boolean = true) {
this.device = device
}
@ContainerDSL
fun ContainerConfig.username(username: String) {
this.username = username
}
@ContainerDSL
fun ContainerConfig.shell(shell: String) {
this.shell = AbsolutePath(shell)
}
@ContainerDSL
fun ContainerConfig.home(home: String) {
this.home = AbsolutePath(home)
}
@ContainerDSL
fun ContainerConfig.executable(path: String, vararg args: String) {
this.path = AbsolutePath(path)
this.args = args.toList()
}
@FilesystemDSL
data class FilesystemConfigs(val configs: MutableList<FilesystemConfig> = 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<String, String>, 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<String, String>, 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<String>? = 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
}

View File

@@ -1,8 +0,0 @@
package moe.rosa.planterette.dsl
import moe.rosa.planterette.PlanteretteConfig
@PlanteretteDSL
fun PlanteretteConfig.metadata() {
}

View File

@@ -0,0 +1,48 @@
package moe.rosa.planterette.dsl
import kotlinx.coroutines.runBlocking
import moe.rosa.planterette.api.Capability
import moe.rosa.planterette.api.Manifest
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.*
import kotlin.script.experimental.dependencies.*
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
import kotlin.script.experimental.jvm.jvm
@KotlinScript(
fileExtension = "plt.kts",
compilationConfiguration = ScriptWithMavenDepsConfiguration::class
)
open class PltRecipe
object ScriptWithMavenDepsConfiguration : ScriptCompilationConfiguration(
{
defaultImports(DependsOn::class, Repository::class)
defaultImports.append("moe.rosa.planterette.dsl.*")
jvm {
dependenciesFromCurrentContext(
"script",
"kotlin-scripting-dependencies"
)
}
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
}
)
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess()
return runBlocking {
resolver.resolveFromScriptSourceAnnotations(annotations)
}.onSuccess {
context.compilationConfiguration.with {
dependencies.append(JvmDependency(it))
}.asSuccess()
}
}

View File

@@ -1,181 +0,0 @@
package moe.rosa.planterette.hakurei
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import java.nio.file.Path
/**
* Points to the file system root.
*/
val ROOT = AbsolutePath("/")
/**
* Points to the directory for system-specific configuration.
*/
val ETC = AbsolutePath("/etc")
/**
* Points to the place for small temporary files.
*/
val TMP = AbsolutePath("/tmp")
/**
* Points to a "tmpfs" file system for system packages to place runtime data, socket files, and similar.
*/
val RUN = AbsolutePath("/run")
/**
* Points to a directory containing per-user runtime directories,
* each usually individually mounted "tmpfs" instances.
*/
val RUN_USER: AbsolutePath = RUN + "user/"
/**
* Points to persistent, variable system data. Writable during normal system operation.
*/
val VAR = AbsolutePath("/var/")
/**
* Points to persistent system data.
*/
val VAR_LIB: AbsolutePath = VAR + "lib/"
/**
* Points to a nonstandard directory that is usually empty.
*/
val VAR_EMPTY: AbsolutePath = VAR + "empty/"
/**
* Points to the root directory for device nodes.
*/
val DEV = AbsolutePath("/dev/")
/**
* Points to a virtual kernel file system exposing the process list and other functionality.
*/
val PROC = AbsolutePath("/proc/")
/**
* Points to a hierarchy below `/proc/` that exposes a number of kernel tunables.
*/
val PROC_SYS: AbsolutePath = PROC + "sys/"
/**
* Points to a virtual kernel file system exposing discovered devices and other functionality.
*/
val SYS = AbsolutePath("/sys")
/**
* Holds a pathname checked to be absolute.
* @constructor checks pathname and returns a new [AbsolutePath] if pathname is absolute.
*/
@Serializable(with = AbsolutePathSerializer::class)
data class AbsolutePath(val pathname: String, @Transient val path: Path = Path.of(pathname)) {
init {
if(!isAbsolute(pathname)) {
throw AbsolutePathException(pathname)
}
}
//TODO discuss if we should keep this operator overloading around, i think it makes things cleaner but ik ozy doesn't like operator overloading
operator fun plus(other: String): AbsolutePath {
return AbsolutePath(pathname + other)
}
operator fun plus(other: AbsolutePath): AbsolutePath {
return AbsolutePath(pathname + other.pathname)
}
companion object {
fun isAbsolute(pathname: String): Boolean {
return Path.of(pathname).isAbsolute
}
}
}
object AbsolutePathSerializer : KSerializer<AbsolutePath> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(this::class.qualifiedName!!, PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: AbsolutePath) {
encoder.encodeString(value.pathname)
}
override fun deserialize(decoder: Decoder): AbsolutePath {
val path = decoder.decodeString()
return AbsolutePath(path)
}
}
/**
* Returned by [AbsolutePath()] and holds the invalid pathname.
*/
data class AbsolutePathException(val pathname: String) : IllegalArgumentException("Path $pathname is not absolute")
@Serializable sealed interface FilesystemConfig
/**
* Represents a host to container bind mount.
* @param target mount point in container, same as source if empty
* @param source host filesystem path to make available to the container
* @param write do not mount target read only
* @param device do not disable device files on target, implies write
* @param ensure create source as a directory if it does not exist
* @param optional skip this mount point if source does not exist
* @param special enable special behavior:
* for autoroot, target must be set to [Filesystem.ROOT];
* for autoetc, target must be set to [Filesystem.ETC]
*/
@Serializable
@SerialName("bind")
data class FSBind(
@SerialName("dst") val target: AbsolutePath? = null,
@SerialName("src") val source: AbsolutePath,
val write: Boolean? = null,
@SerialName("dev") val device: Boolean? = null,
val ensure: Boolean? = null,
val optional: Boolean? = null,
val special: Boolean? = null,
) : FilesystemConfig
/**
* Represents an ephemeral (temporary) container mount point.
* @param target mount point in container
* @param write do not mount filesystem read-only
* @param size upper limit on the size of the filesystem
* @param perm initial permission bits of the new filesystem
*/
@Serializable
@SerialName("ephemeral")
data class FSEphemeral(
@SerialName("dst") val target: AbsolutePath,
val write: Boolean,
val size: Int? = null,
val perm: Int,
) : FilesystemConfig
/**
* Represents a symlink in the container filesystem.
* @param target link path in container
* @param linkname linkname the symlink points to
* @param dereference whether to dereference linkname before creating the link
*/
@Serializable
@SerialName("link")
data class FSLink(
@SerialName("dst") val target: AbsolutePath,
val linkname: String,
val dereference: Boolean,
) : FilesystemConfig
/**
* Represents an overlay mount point.
* @param target mount point in container
* @param lower any filesystem, does not need to be on a writable filesystem
* @param upper the upperdir is normally on a writable filesystem, leave as null to mount Lower readonly
* @param work the workdir needs to be an empty directory on the same filesystem as `upper`, must not be null if `upper` is populated
*/
@Serializable
@SerialName("overlay")
data class FSOverlay(
@SerialName("dst") val target: AbsolutePath,
val lower: List<AbsolutePath>,
val upper: AbsolutePath? = null,
val work: AbsolutePath? = null,
) : FilesystemConfig

View File

@@ -1,162 +0,0 @@
package moe.rosa.planterette.hakurei
import kotlinx.serialization.*
import java.time.Duration
val WAIT_DELAY_DEFAULT = Duration.ofSeconds(1)!!
val WAIT_DELAY_MAX = Duration.ofSeconds(30)!!
const val IDENTITY_MIN = 0
const val IDENTITY_MAX = 9999
/**
* [HakureiConfig] configures an application container.
* @param id Reverse-DNS style configured arbitrary identifier string.
* Passed to wayland security-context-v1 and used as part of defaults in dbus session proxy.
* @param enablements System services to make available in the container.
* @param sessionBus Session D-Bus proxy configuration.
* If set to null, session bus proxy assume built-in defaults.
* @param systemBus System D-Bus proxy configuration.
* If set to nil, system bus proxy is disabled.
* @param directWayland Direct access to wayland socket, no attempt is made to attach security-context-v1
* and the bare socket is made available to the container.
* @param extraPerms Extra acl update ops to perform before setuid.
* @param identity Numerical application id, passed to hsu, used to derive init user namespace credentials.
* @param groups Init user namespace supplementary groups inherited by all container processes.
* @param container High level container configuration.
*/
@Serializable
data class HakureiConfig(
var id: String? = 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,
@SerialName("extra_perms") var extraPerms: List<ExtraPermsConfig>? = null,
var identity: Int? = null,
var groups: List<String>? = null,
var container: ContainerConfig? = null,
)
/**
* Describes the container configuration to be applied to the container.
* @param hostname Container UTS namespace hostname.
* @param waitDelay Duration in nanoseconds to wait for after interrupting the initial process.
* Defaults to [WAIT_DELAY_DEFAULT] if less than or equals to zero,
* or [WAIT_DELAY_MAX] if greater than [WAIT_DELAY_MAX].
*
* @param seccompCompat Emit Flatpak-compatible seccomp filter programs.
* @param devel Allow ptrace and friends.
* @param userns Allow userns creation and container setup syscalls.
* @param hostNet Share host net namespace.
* @param hostAbstract Share abstract unix socket scope.
* @param tty Allow dangerous terminal I/O (faking input).
* @param multiarch Allow multiarch.
*
* @param env Initial process environment variables.
*
* @param mapRealUid Map target user uid to privileged user uid in the container user namespace.
* Some programs fail to connect to dbus session running as a different uid,
* this option works around it by mapping priv-side caller uid in container.
*
* @param device Mount `/dev/` from the init mount namespace as-is in the container mount namespace.
* @param filesystem Container mount points.
* If the first element targets /, it is inserted early and excluded from path hiding.
* @param username String used as the username of the emulated user, validated against the default `NAME_REGEX` from adduser.
* @param shell Pathname of shell in the container filesystem to use for the emulated user.
* @param home Directory in the container filesystem to enter and use as the home directory of the emulated user.
* @param path Pathname to executable file in the container filesystem.
* @param args Final args passed to the initial program.
*/
@Serializable
data class ContainerConfig(
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,
var env: Map<String, String>? = null,
@SerialName("map_real_uid") var mapRealUid: Boolean? = null,
var device: Boolean? = null,
var filesystem: List<FilesystemConfig>? = null,
var username: String? = "chronos",
var shell: AbsolutePath? = null,
var home: AbsolutePath? = null,
var path: AbsolutePath? = null,
var args: List<String>? = null,
)
/**
* Describes an acl update op.
*/
@Serializable
data class ExtraPermsConfig(
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)
buffer.append("---")
if(ensure == true) {
buffer.append("+")
}
buffer.append(":")
buffer.append(path.toString())
if(read == true) {
buffer.setCharAt(0, 'r')
}
if(write == true) {
buffer.setCharAt(1, 'w')
}
if(execute == true) {
buffer.setCharAt(2, 'x')
}
return buffer.toString()
}
}
/**
* Configures the `xdg-dbus-proxy` process.
* @param see Set `see` policy for `NAME` (`--see=NAME`)
* @param talk Set `talk` policy for `NAME` (`--talk=NAME`)
* @param own Set `own` policy for `NAME` (`--own=NAME)
* @param call Set `RULE` for calls on `NAME` (`--call=NAME=RULE`)
* @param broadcast Set `RULE` for broadcasts from `NAME` (`--broadcast=NAME=RULE`)
* @param log Turn on logging (`--log`)
* @param filter Enable filtering (`--filter`)
*/
@Serializable
data class DBusConfig(
var see: List<String>? = null,
var talk: List<String>? = null,
var own: List<String>? = null,
var call: Map<String, String>? = null,
var broadcast: Map<String, String>? = null,
var log: Boolean? = null,
var filter: Boolean? = null,
)
/**
* Represents an optional host service to export to the target user.
*/
@Serializable
data class Enablements(
var wayland: Boolean? = null,
var x11: Boolean? = null,
var dbus: Boolean? = null,
var pulse: Boolean? = null,
)

View File

@@ -0,0 +1,19 @@
package moe.rosa.planterette.jni
import java.io.File
import java.nio.file.Files
object GoFile {
external fun write(fd: Int, str: String)
init {
val libname = "pltbuild.so"
val url = GoFile::class.java.getResource("/$libname")
val tmpDir = Files.createTempDirectory("pltbuild").toFile()
tmpDir.deleteOnExit()
val nativeLibTmpFile = File(tmpDir, libname)
url?.openStream().use {
Files.copy(it!!, nativeLibTmpFile.toPath())
}
System.load(nativeLibTmpFile.absolutePath)
}
}

View File

@@ -1,111 +0,0 @@
import moe.rosa.planterette.dsl.*
import moe.rosa.planterette.dsl.DSLEnablements.*
import kotlin.test.*
class HakureiDSLTest {
companion object {
val HAKUREI_DSL_TEST = planterette {
hakurei("org.chromium.Chromium") {
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()
}
}
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()
executable("/run/current-system/sw/bin/chromium",
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
)
username("chronos")
shell("/run/current-system/sw/bin/zsh")
home("/data/data/org.chromium.Chromium")
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("/var/lib/hakurei/base/org.nixos/ro-store")
upper("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper")
work("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work")
}
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)
}
}

View File

@@ -1,194 +0,0 @@
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import moe.rosa.planterette.hakurei.*
import org.junit.jupiter.api.assertDoesNotThrow
import kotlin.test.*
class HakureiTest {
companion object {
val TEMPLATE_DATA = HakureiConfig(
id = "org.chromium.Chromium",
enablements = Enablements(
wayland = true,
dbus = true,
pulse = true
),
sessionBus = DBusConfig(
see = null,
talk = listOf(
"org.freedesktop.Notifications",
"org.freedesktop.FileManager1",
"org.freedesktop.ScreenSaver",
"org.freedesktop.secrets",
"org.kde.kwalletd5",
"org.kde.kwalletd6",
"org.gnome.SessionManager"
),
own = listOf(
"org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
"org.mpris.MediaPlayer2.chromium.*"
),
call = mapOf(
"org.freedesktop.portal.*" to "*"
),
broadcast = mapOf(
"org.freedesktop.portal.*" to "@/org/freedesktop/portal/*"
),
filter = true
),
systemBus = DBusConfig(
see = null,
talk = listOf(
"org.bluez",
"org.freedesktop.Avahi",
"org.freedesktop.UPower"
),
own = null,
call = null,
broadcast = null,
filter = true
),
extraPerms = listOf(
ExtraPermsConfig(
ensure = true,
path = AbsolutePath("/var/lib/hakurei/u0"),
read = null,
write = null,
execute = true,
),
ExtraPermsConfig(
ensure = null,
path = AbsolutePath("/var/lib/hakurei/u0/org.chromium.Chromium"),
read = true,
write = true,
execute = true,
),
),
identity = 9,
groups = listOf(
"video",
"dialout",
"plugdev"
),
container = ContainerConfig(
hostname = "localhost",
waitDelay = -1,
seccompCompat = true,
devel = true,
userns = true,
hostNet = true,
hostAbstract = true,
tty = true,
multiarch = true,
env = mapOf(
"GOOGLE_API_KEY" to "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
"GOOGLE_DEFAULT_CLIENT_ID" to "77185425430.apps.googleusercontent.com",
"GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT"
),
mapRealUid = true,
device = true,
filesystem = listOf(
FSBind(
target = AbsolutePath("/"),
source = AbsolutePath("/var/lib/hakurei/base/org.debian"),
write = true,
special = true,
),
FSBind(
target = AbsolutePath("/etc/"),
source = AbsolutePath("/etc/"),
special = true,
),
FSEphemeral(
target = AbsolutePath("/tmp/"),
write = true,
perm = 493
),
FSOverlay(
target = AbsolutePath("/nix/store"),
lower = listOf(
AbsolutePath("/var/lib/hakurei/base/org.nixos/ro-store")
),
upper = AbsolutePath("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/upper"),
work = AbsolutePath("/var/lib/hakurei/nix/u0/org.chromium.Chromium/rw-store/work")
),
FSLink(
target = AbsolutePath("/run/current-system"),
linkname = "/run/current-system",
dereference = true
),
FSLink(
target = AbsolutePath("/run/opengl-driver"),
linkname = "/run/opengl-driver",
dereference = true
),
FSBind(
target = AbsolutePath("/data/data/org.chromium.Chromium"),
source = AbsolutePath("/var/lib/hakurei/u0/org.chromium.Chromium"),
write = true,
ensure = true,
),
FSBind(
source = AbsolutePath("/dev/dri"),
device = true,
optional = true
)
),
username = "chronos",
shell = AbsolutePath("/run/current-system/sw/bin/zsh"),
home = AbsolutePath("/data/data/org.chromium.Chromium"),
path = AbsolutePath("/run/current-system/sw/bin/chromium"),
args = listOf(
"chromium",
"--ignore-gpu-blocklist",
"--disable-smooth-scrolling",
"--enable-features=UseOzonePlatform",
"--ozone-platform=wayland"
),
)
)
val TEMPLATE_JSON = ProcessBuilder("hakurei", "template")
.start()
.inputStream
.readAllBytes()
.toString(Charsets.UTF_8)
val format = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
}
@OptIn(ExperimentalSerializationApi::class)
@Test
fun deserializeTest() {
println(TEMPLATE_JSON)
val want = format.decodeFromString<HakureiConfig>(TEMPLATE_JSON)
assertEquals(TEMPLATE_DATA, want)
}
@OptIn(ExperimentalSerializationApi::class)
@Test
fun serializeTest() {
val encoded = format.encodeToString(TEMPLATE_DATA)
val decoded = format.decodeFromString<HakureiConfig>(encoded)
assertEquals(TEMPLATE_DATA, decoded)
}
@Test
fun absolutePathTest() {
assertDoesNotThrow {
AbsolutePath("/test/absolutepath")
}
assertFailsWith(AbsolutePathException::class) {
AbsolutePath("./../../../../")
}
assertEquals(AbsolutePath("/test/absolutepath"), AbsolutePath("/test/") + "absolutepath")
}
@Test
fun extraPermsTest() {
assertIs<String>(TEMPLATE_DATA.extraPerms.toString())
}
}