Compare commits
9 Commits
bc08a46f28
...
3efcca8651
| Author | SHA1 | Date | |
|---|---|---|---|
| 3efcca8651 | |||
| df5e191582 | |||
| 21738818d5 | |||
| 9215adc740 | |||
| a2e8249003 | |||
| 0d8b0eaecd | |||
| d1ccb1c762 | |||
| bc1c7172d4 | |||
| b57edb8d3a |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,12 +1,14 @@
|
||||
.gradle
|
||||
gradlew.bat
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
/.idea/
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
2
.idea/.gitignore
generated
vendored
2
.idea/.gitignore
generated
vendored
@ -1,5 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Environment-dependent path to Maven home directory
|
||||
/mavenHomeManager.xml
|
||||
|
||||
10
.idea/codeStyles/Project.xml
generated
Normal file
10
.idea/codeStyles/Project.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
8
.idea/dictionaries/project.xml
generated
8
.idea/dictionaries/project.xml
generated
@ -1,8 +0,0 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>buildsrc</w>
|
||||
<w>planterette</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
17
.idea/gradle.xml
generated
17
.idea/gradle.xml
generated
@ -4,28 +4,11 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<compositeConfiguration>
|
||||
<compositeBuild compositeDefinitionSource="SCRIPT">
|
||||
<builds>
|
||||
<build path="$PROJECT_DIR$/buildSrc" name="buildSrc">
|
||||
<projects>
|
||||
<project path="$PROJECT_DIR$/buildSrc" />
|
||||
</projects>
|
||||
</build>
|
||||
</builds>
|
||||
</compositeBuild>
|
||||
</compositeConfiguration>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/libplt" />
|
||||
<option value="$PROJECT_DIR$/plt" />
|
||||
<option value="$PROJECT_DIR$/plt-build" />
|
||||
<option value="$PROJECT_DIR$/plt-fetch" />
|
||||
<option value="$PROJECT_DIR$/plt-pkg" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.2.20" />
|
||||
</component>
|
||||
</project>
|
||||
5
.idea/misc.xml
generated
5
.idea/misc.xml
generated
@ -1,7 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_24" default="true" project-jdk-name="openjdk-24" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
21
README.md
21
README.md
@ -1,23 +1,2 @@
|
||||
# planterette
|
||||
|
||||
This project uses [Gradle](https://gradle.org/).
|
||||
To build and run the application, use the *Gradle* tool window by clicking the Gradle icon in the right-hand toolbar,
|
||||
or run it directly from the terminal:
|
||||
|
||||
* Run `./gradlew run` to build and run the application.
|
||||
* Run `./gradlew build` to only build the application.
|
||||
* Run `./gradlew check` to run all checks, including tests.
|
||||
* Run `./gradlew clean` to clean all build outputs.
|
||||
|
||||
Note the usage of the Gradle Wrapper (`./gradlew`).
|
||||
This is the suggested way to use Gradle in production projects.
|
||||
|
||||
[Learn more about the Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html).
|
||||
|
||||
[Learn more about Gradle tasks](https://docs.gradle.org/current/userguide/command_line_interface.html#common_tasks).
|
||||
|
||||
This project follows the suggested multi-module setup and consists of the `app` and `api` subprojects.
|
||||
The shared build logic was extracted to a convention plugin located in `buildSrc`.
|
||||
|
||||
This project uses a version catalog (see `gradle/libs.versions.toml`) to declare and version dependencies
|
||||
and both a build cache and a configuration cache (see `gradle.properties`).
|
||||
46
build.gradle.kts
Normal file
46
build.gradle.kts
Normal file
@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.10"
|
||||
kotlin("plugin.serialization") version "2.2.20"
|
||||
}
|
||||
|
||||
group = "moe.rosa"
|
||||
version = "0.1.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
implementation(kotlin("reflect"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
dependsOn(testGo)
|
||||
}
|
||||
tasks.classes {
|
||||
dependsOn(compileGo)
|
||||
}
|
||||
kotlin {
|
||||
jvmToolchain(24)
|
||||
}
|
||||
|
||||
val compileGo = tasks.register("compileGo", Exec::class) {
|
||||
group = "go"
|
||||
workingDir(layout.projectDirectory)
|
||||
commandLine("/usr/bin/go", "build", "-o", "build/go/planterette", "./src/main/go")
|
||||
}
|
||||
|
||||
val testGo = tasks.register("testGo", Exec::class) {
|
||||
group = "go"
|
||||
workingDir(layout.projectDirectory)
|
||||
commandLine("/usr/bin/go", "test", "./src/main/go")
|
||||
}
|
||||
|
||||
val runGo = tasks.register("runGo", Exec::class) {
|
||||
group = "go"
|
||||
workingDir(layout.projectDirectory)
|
||||
commandLine("/usr/bin/go", "run", "./src/main/go")
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(24)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinGradlePlugin)
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
dependencyResolutionManagement {
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "buildSrc"
|
||||
@ -1,15 +0,0 @@
|
||||
package buildsrc.convention
|
||||
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events(
|
||||
TestLogEvent.FAILED,
|
||||
TestLogEvent.PASSED,
|
||||
TestLogEvent.SKIPPED
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1 @@
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
kotlin.mpp.applyDefaultHierarchyTemplate=false
|
||||
kotlin.code.style=official
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
[versions]
|
||||
kotlin = "2.2.0"
|
||||
kotlinxDatetime = "0.6.1"
|
||||
kotlinxSerializationJSON = "1.7.3"
|
||||
kotlinxCoroutines = "1.9.0"
|
||||
kotlinxIo = "0.8.0"
|
||||
|
||||
[libraries]
|
||||
kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
kotlinxDatetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
kotlinxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJSON" }
|
||||
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||
kotlinxIo = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIo" }
|
||||
|
||||
[bundles]
|
||||
kotlinxEcosystem = ["kotlinxDatetime", "kotlinxSerialization", "kotlinxCoroutines", "kotlinxIo"]
|
||||
|
||||
[plugins]
|
||||
kotlinPluginSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
kotlinPluginMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,4 +1,4 @@
|
||||
#Tue Aug 26 23:15:20 CDT 2025
|
||||
#Fri Sep 26 23:47:58 CDT 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
|
||||
43
gradlew
vendored
43
gradlew
vendored
@ -15,8 +15,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -57,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@ -82,11 +80,13 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -133,29 +133,22 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@ -200,15 +193,11 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
@ -216,12 +205,6 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
||||
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@ -1,26 +0,0 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
alias(libs.plugins.kotlinPluginSerialization)
|
||||
}
|
||||
kotlin {
|
||||
jvm()
|
||||
val nativeTarget = if(System.getProperty("os.arch") == "aarch64") {
|
||||
linuxArm64("native")
|
||||
} else {
|
||||
linuxX64("native")
|
||||
}
|
||||
nativeTarget.binaries {
|
||||
sharedLib {
|
||||
baseName = "libplt"
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.bundles.kotlinxEcosystem)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(libs.bundles.kotlinxEcosystem)
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
import app.hakurei.planterette.api.AbsolutePath.Companion.isAbsolute
|
||||
import kotlinx.io.files.Path
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
/**
|
||||
* AbsolutePath 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(pathname)) {
|
||||
init {
|
||||
if(!isAbsolute(pathname)) {
|
||||
throw AbsolutePathException(pathname)
|
||||
}
|
||||
}
|
||||
operator fun plus(other: String): AbsolutePath {
|
||||
return AbsolutePath(pathname + other)
|
||||
}
|
||||
companion object {
|
||||
fun isAbsolute(pathname: String): Boolean {
|
||||
return Path(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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AbsolutePathException is returned by @see AbsolutePath() and holds the invalid pathname.
|
||||
*/
|
||||
data class AbsolutePathException(val pathname: String) : IllegalArgumentException("Path $pathname is not absolute")
|
||||
@ -1,209 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* HakureiConfig is used to seal an app implementation.
|
||||
* @param id reverse-DNS style arbitrary identifier string from config; Passed to wayland security-context-v1 as application ID and used as part of defaults in dbus session proxy
|
||||
*
|
||||
* @param path absolute path to executable file
|
||||
* @param args final args passed to container init
|
||||
*
|
||||
* @param enablements system services to make available in the container
|
||||
*
|
||||
* @param sessionBus session D-Bus proxy configuration; null makes session bus proxy assume built-in defaults
|
||||
* @param systemBus system D-Bus proxy configuration; null disables system bus proxy
|
||||
* @param directWayland direct access to the wayland socket; when this gets set no attempt is made to attach security-context-v1 and the bare socket is mounted to the sandbox
|
||||
*
|
||||
* @param username passwd username in container, defaults to passwd name of target uid or chronos
|
||||
* @param shell absolute path to shell
|
||||
* @param data absolute path to home directory in the init mount namespace
|
||||
* @param dir directory to ender and use as home in the container mount namespace, null for Data
|
||||
* @param extraPerms extra ACL ops, dispatches before container init
|
||||
*
|
||||
* @param identity numerical application id, used for init user namespace credentials
|
||||
* @param groups list of supplementary groups inherited by container processes
|
||||
*
|
||||
* @param container abstract container configuration baseline
|
||||
*/
|
||||
@Serializable
|
||||
data class HakureiConfig(
|
||||
val id: String,
|
||||
|
||||
val path: AbsolutePath? = null,
|
||||
val args: List<String>,
|
||||
|
||||
val enablements: Byte,
|
||||
|
||||
@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,
|
||||
val data: AbsolutePath,
|
||||
val dir: AbsolutePath,
|
||||
@SerialName("extra_perms") val extraPerms: List<ExtraPermConfig>? = null,
|
||||
|
||||
val identity: Int,
|
||||
val groups: List<String>,
|
||||
|
||||
val container: ContainerConfig
|
||||
) {
|
||||
@Serializable
|
||||
data class DbusConfig(
|
||||
val see: List<String>? = null,
|
||||
val talk: List<String>? = null,
|
||||
val own: List<String>? = null,
|
||||
val call: Map<String, String>? = null,
|
||||
val broadcast: Map<String, String>? = null,
|
||||
|
||||
val log: Boolean? = null,
|
||||
val filter: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* ExtraPermConfig describes an ACL update op.
|
||||
*/
|
||||
@Serializable
|
||||
data class ExtraPermConfig(
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* ContainerConfig describes the container configuration baseline to which the app implementation adds upon.
|
||||
* @param hostname container hostname
|
||||
* @param waitDelay duration to wait for after interrupting a container's initial process in nanoseconds; a negative value causes the container to be terminated immediately on cancellation
|
||||
* @param seccompFlags extra seccomp flags
|
||||
* @param seccompPresets extra seccomp presets
|
||||
* @param seccompCompat disable project-specific filter extensions
|
||||
* @param devel allow ptrace and friends
|
||||
* @param userns allow userns creation in container
|
||||
* @param net share host net namespace
|
||||
* @param tty allow dangerous terminal I/O
|
||||
* @param multiarch allow multiarch
|
||||
*
|
||||
* @param env initial process environment variables
|
||||
* @param mapRealUid map target user uid to privileged user uid in the user namespace
|
||||
*
|
||||
* @param device pass through all devices
|
||||
* @param filesystem container host filesystem bind mounts
|
||||
* @param link create symlinks inside container filesystem
|
||||
*
|
||||
* @param autoRoot automatically bind mount top-level directories to container root; the zero value disables this behaviour
|
||||
* @param rootFlags extra flags for AutoRoot
|
||||
*
|
||||
* @param etc read-only /etc directory
|
||||
* @param autoEtc automatically set up /etc symlinks
|
||||
*/
|
||||
@Serializable
|
||||
data class ContainerConfig(
|
||||
val hostname: String?,
|
||||
@SerialName("wait_delay") val waitDelay: Int? = null,
|
||||
@SerialName("seccomp_flags") val seccompFlags: Int,
|
||||
@SerialName("seccomp_presets") val seccompPresets: Int,
|
||||
@SerialName("seccomp_compat") val seccompCompat: Boolean? = null,
|
||||
|
||||
val devel: Boolean? = null,
|
||||
val userns: Boolean? = null,
|
||||
val net: Boolean? = null,
|
||||
val tty: Boolean? = null,
|
||||
val multiarch: Boolean? = null,
|
||||
|
||||
val env: Map<String, String>,
|
||||
@SerialName("map_real_uid") val mapRealUid: Boolean,
|
||||
|
||||
val device: Boolean?,
|
||||
val filesystem: List<FilesystemConfig>,
|
||||
@SerialName("symlink") val link: List<LinkConfig>,
|
||||
|
||||
@SerialName("auto_root") val autoRoot: AbsolutePath,
|
||||
@SerialName("root_flags") val rootFlags: Int,
|
||||
|
||||
val etc: AbsolutePath?,
|
||||
|
||||
@SerialName("auto_etc") val autoEtc: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* FilesystemConfig is an abstract representation of a bind mount.
|
||||
* @param dst mount point in container, same as src if empty
|
||||
* @param src host filesystem path to make available to the container
|
||||
* @param write do not mount filesystem read-only
|
||||
* @param device do not disable device files
|
||||
* @param must fail if the bind mount cannot be established for any reason
|
||||
*/
|
||||
@Serializable
|
||||
data class FilesystemConfig(
|
||||
val dst: AbsolutePath? = null,
|
||||
val src: AbsolutePath,
|
||||
val write: Boolean? = null,
|
||||
@SerialName("dev") val device: Boolean? = null,
|
||||
@SerialName("require") val must: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* @param target symlink target in container
|
||||
* @param linkname linkname the symlink points to; prepend '*' to dereference an absolute pathname on host
|
||||
*/
|
||||
@Serializable
|
||||
data class LinkConfig(
|
||||
val target: AbsolutePath,
|
||||
val linkname: String
|
||||
)
|
||||
enum class Enablement(val value: Int) {
|
||||
Wayland(1 shl 0),
|
||||
X11(1 shl 1),
|
||||
DBus(1 shl 2),
|
||||
Pulse(1 shl 3);
|
||||
|
||||
companion object {
|
||||
fun enablements(vararg enablements: Enablement): Byte {
|
||||
return enablements.orOf(Enablement::value).toByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SeccompFilterPreset(val value: Int) {
|
||||
Ext(1 shl 0),
|
||||
DenyNS(1 shl 1),
|
||||
DenyTTY(1 shl 2),
|
||||
DenyDevel(1 shl 3),
|
||||
Linux32(1 shl 4);
|
||||
|
||||
companion object {
|
||||
fun filterPresets(vararg filterPresets: SeccompFilterPreset): Int {
|
||||
return filterPresets.orOf(SeccompFilterPreset::value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class HakureiExportFlag(val value: Int) {
|
||||
Multiarch(1 shl 0),
|
||||
CAN(1 shl 1),
|
||||
Bluetooth(1 shl 2);
|
||||
|
||||
companion object {
|
||||
fun exportFlags(vararg exportFlags: HakureiExportFlag): Int {
|
||||
return exportFlags.orOf(HakureiExportFlag::value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class RootFlag(val value: Int) {
|
||||
Optional(1 shl 0),
|
||||
Writable(1 shl 1),
|
||||
Device(1 shl 2);
|
||||
|
||||
companion object {
|
||||
fun rootFlags(vararg rootFlags: RootFlag): Int {
|
||||
return rootFlags.orOf(RootFlag::value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PackageManifest(
|
||||
var hakureiConfig: HakureiConfig,
|
||||
var id: String,
|
||||
var version: Version,
|
||||
var name: String,
|
||||
var description: String,
|
||||
var architecture: MutableList<Architecture>,
|
||||
var baseImage: BaseImage
|
||||
) {
|
||||
@Serializable
|
||||
data class Version(val version: String, val canonicalVersion: UInt)
|
||||
|
||||
@Serializable
|
||||
enum class Architecture {
|
||||
X86_64,
|
||||
ARM64,
|
||||
}
|
||||
@Serializable
|
||||
data class BaseImage(val type: Type) {
|
||||
@Serializable
|
||||
enum class Type {
|
||||
DEBIAN,
|
||||
CHIMERA,
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
val fileList = listOf("planterette.json", "icon.png", "image.tar")
|
||||
val signatureFile = "package.sig"
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
@Suppress("unused")
|
||||
object Paths {
|
||||
const val FHS_ROOT = "/"
|
||||
const val FHS_ETC = "/etc/"
|
||||
const val FHS_TMP = "/tmp/"
|
||||
|
||||
const val FHS_RUN = "/run/"
|
||||
const val FHS_RUN_USER = FHS_RUN + "user/"
|
||||
|
||||
const val FHS_USR = "/usr/"
|
||||
const val FHS_USR_BIN = FHS_USR + "bin/"
|
||||
|
||||
const val FHS_VAR = "/var/"
|
||||
const val FHS_VAR_LIB = FHS_VAR + "lib/"
|
||||
const val FHS_VAR_EMPTY = FHS_VAR + "empty/"
|
||||
|
||||
const val FHS_DEV = "/dev/"
|
||||
const val FHS_PROC = "/proc/"
|
||||
const val FHS_PROC_SYS = FHS_PROC + "sys/"
|
||||
const val FHS_SYS = "/sys/"
|
||||
|
||||
const val NONEXISTENT = FHS_PROC + "nonexistent"
|
||||
const val SYSROOT_DIR = "sysroot"
|
||||
const val HOST_DIR = "host"
|
||||
const val HOST_PATH = FHS_ROOT + HOST_DIR
|
||||
const val SYSROOT_PATH = FHS_ROOT + SYSROOT_DIR
|
||||
|
||||
val AbsFHSRoot = AbsolutePath(FHS_ROOT)
|
||||
val AbsFHSEtc = AbsolutePath(FHS_ETC)
|
||||
val AbsFHSTmp = AbsolutePath(FHS_TMP)
|
||||
|
||||
val AbsFHSRun = AbsolutePath(FHS_RUN)
|
||||
val AbsFHSRunUser = AbsolutePath(FHS_RUN_USER)
|
||||
|
||||
val AbsFHSUsrBin = AbsolutePath(FHS_USR_BIN)
|
||||
|
||||
val AbsFHSVar = AbsolutePath(FHS_VAR)
|
||||
val AbsFHSVarLib = AbsolutePath(FHS_VAR_LIB)
|
||||
|
||||
val AbsFHSDev = AbsolutePath(FHS_DEV)
|
||||
val AbsFHSProc = AbsolutePath(FHS_PROC)
|
||||
val AbsFHSSys = AbsolutePath(FHS_SYS)
|
||||
|
||||
val AbsNonexistent = AbsolutePath(NONEXISTENT)
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
inline fun <T> Array<out T>.orOf(selector: (T) -> Int): Int {
|
||||
var sum = 0
|
||||
for (element in this) {
|
||||
sum = sum or selector(element)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class AbsolutePathTest {
|
||||
@Test
|
||||
fun testAbsolutePath() {
|
||||
val path = AbsolutePath("/dev/null")
|
||||
assertIs<AbsolutePath>(path)
|
||||
}
|
||||
@Test
|
||||
fun testRelativePath() {
|
||||
assertFailsWith(AbsolutePathException::class) {
|
||||
AbsolutePath("./test")
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun testPathJoin() {
|
||||
val want = AbsolutePath("/dev/null")
|
||||
val base = AbsolutePath("/dev/")
|
||||
assertEquals(want, base + "null")
|
||||
}
|
||||
}
|
||||
@ -1,333 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
import app.hakurei.planterette.api.HakureiConfig.Enablement.*
|
||||
import app.hakurei.planterette.api.HakureiConfig.SeccompFilterPreset.*
|
||||
import app.hakurei.planterette.api.HakureiConfig.HakureiExportFlag.*
|
||||
import app.hakurei.planterette.api.HakureiConfig.RootFlag.*
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class HakureiConfigTest {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val format = Json {
|
||||
explicitNulls = false
|
||||
prettyPrint = true
|
||||
}
|
||||
val want: String = """
|
||||
{
|
||||
"id": "org.chromium.Chromium",
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
],
|
||||
"enablements": 13,
|
||||
"session_bus": {
|
||||
"see": null,
|
||||
"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.*": "*"
|
||||
},
|
||||
"broadcast": {
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"
|
||||
},
|
||||
"filter": true
|
||||
},
|
||||
"system_bus": {
|
||||
"see": null,
|
||||
"talk": [
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower"
|
||||
],
|
||||
"own": null,
|
||||
"call": null,
|
||||
"broadcast": null,
|
||||
"filter": true
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
}
|
||||
],
|
||||
"identity": 9,
|
||||
"groups": [
|
||||
"video",
|
||||
"dialout",
|
||||
"plugdev"
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"wait_delay": -1,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"seccomp_compat": true,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": true,
|
||||
"tty": true,
|
||||
"multiarch": true,
|
||||
"env": {
|
||||
"GOOGLE_API_KEY": "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID": "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET": "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
},
|
||||
"map_real_uid": true,
|
||||
"device": true,
|
||||
"filesystem": [
|
||||
{
|
||||
"dst": "/tmp/",
|
||||
"src": "/proc/nonexistent",
|
||||
"write": true
|
||||
},
|
||||
{
|
||||
"src": "/nix/store"
|
||||
},
|
||||
{
|
||||
"src": "/run/current-system"
|
||||
},
|
||||
{
|
||||
"src": "/run/opengl-driver"
|
||||
},
|
||||
{
|
||||
"src": "/var/db/nix-channels"
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
{
|
||||
"src": "/dev/dri",
|
||||
"dev": true
|
||||
}
|
||||
],
|
||||
"symlink": [
|
||||
{
|
||||
"target": "/run/user/65534",
|
||||
"linkname": "/run/user/150"
|
||||
}
|
||||
],
|
||||
"auto_root": "/var/lib/hakurei/base/org.debian",
|
||||
"root_flags": 2,
|
||||
"etc": "/etc/",
|
||||
"auto_etc": true
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val testConfig = HakureiConfig(
|
||||
id = "org.chromium.Chromium",
|
||||
path = Paths.AbsFHSRun + "current-system/sw/bin/chromium",
|
||||
args = listOf(
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland",
|
||||
),
|
||||
enablements = HakureiConfig.Enablement.enablements(Wayland, DBus, Pulse),
|
||||
sessionBus = HakureiConfig.DbusConfig(
|
||||
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/*"),
|
||||
log = false,
|
||||
filter = true,
|
||||
),
|
||||
systemBus = HakureiConfig.DbusConfig(
|
||||
talk = listOf("org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"),
|
||||
log = false,
|
||||
filter = true,
|
||||
),
|
||||
directWayland = false,
|
||||
|
||||
username = "chronos",
|
||||
shell = Paths.AbsFHSRun + "current-system/sw/bin/zsh",
|
||||
data = Paths.AbsFHSVarLib + "hakurei/u0/org.chromium.Chromium",
|
||||
dir = AbsolutePath("/data/data/org.chromium.Chromium"),
|
||||
extraPerms = listOf(
|
||||
HakureiConfig.ExtraPermConfig(path = Paths.AbsFHSVarLib + "hakurei/u0", ensure = true, execute = true),
|
||||
HakureiConfig.ExtraPermConfig(path = Paths.AbsFHSVarLib + "hakurei/u0/org.chromium.Chromium", read = true, write = true, execute = true)
|
||||
),
|
||||
identity = 9,
|
||||
groups = listOf("video", "dialout", "plugdev"),
|
||||
container = HakureiConfig.ContainerConfig(
|
||||
hostname = "localhost",
|
||||
devel = true,
|
||||
userns = true,
|
||||
net = true,
|
||||
device = true,
|
||||
waitDelay = -1,
|
||||
seccompFlags = HakureiConfig.HakureiExportFlag.exportFlags(Multiarch),
|
||||
seccompPresets = HakureiConfig.SeccompFilterPreset.filterPresets(Ext),
|
||||
seccompCompat = true,
|
||||
tty = true,
|
||||
multiarch = true,
|
||||
mapRealUid = true,
|
||||
env = mapOf(
|
||||
"GOOGLE_API_KEY" to "AIzaSyBHDrl33hwRp4rMQY0ziRbj8K9LPA6vUCY",
|
||||
"GOOGLE_DEFAULT_CLIENT_ID" to "77185425430.apps.googleusercontent.com",
|
||||
"GOOGLE_DEFAULT_CLIENT_SECRET" to "OTJgUOQcT7lO7GsGZq2G4IlT"
|
||||
),
|
||||
filesystem = listOf(
|
||||
HakureiConfig.FilesystemConfig(dst = Paths.AbsFHSTmp, src = Paths.AbsNonexistent, write = true),
|
||||
HakureiConfig.FilesystemConfig(src = AbsolutePath("/nix/store")),
|
||||
HakureiConfig.FilesystemConfig(src = Paths.AbsFHSRun + "current-system"),
|
||||
HakureiConfig.FilesystemConfig(src = Paths.AbsFHSRun + "opengl-driver"),
|
||||
HakureiConfig.FilesystemConfig(src = Paths.AbsFHSVar + "db/nix-channels"),
|
||||
HakureiConfig.FilesystemConfig(src = Paths.AbsFHSVarLib + "hakurei/u0/org.chromium.Chromium", dst = AbsolutePath("/data/data/org.chromium.Chromium"), write = true, must = true),
|
||||
HakureiConfig.FilesystemConfig(src = Paths.AbsFHSDev + "dri", device = true)
|
||||
),
|
||||
link = listOf(HakureiConfig.LinkConfig(Paths.AbsFHSRunUser + "65534", Paths.FHS_RUN_USER + "150")),
|
||||
autoRoot = Paths.AbsFHSVarLib + "hakurei/base/org.debian",
|
||||
rootFlags = HakureiConfig.RootFlag.rootFlags(Writable),
|
||||
etc = Paths.AbsFHSEtc,
|
||||
autoEtc = true
|
||||
)
|
||||
)
|
||||
|
||||
internal fun compare(want: HakureiConfig, test: HakureiConfig) {
|
||||
assertEquals(want.id, test.id, "id")
|
||||
|
||||
assertEquals(want.path, test.path, "path")
|
||||
assertEquals(want.args, test.args, "args")
|
||||
|
||||
assertEquals(want.enablements, test.enablements, "enablements")
|
||||
|
||||
assertEquals(want.sessionBus?.see, test.sessionBus?.see, "sessionBus.see")
|
||||
assertEquals(want.sessionBus?.talk, test.sessionBus?.talk, "sessionBus.talk")
|
||||
assertEquals(want.sessionBus?.own, test.sessionBus?.own, "sessionBus.own")
|
||||
assertEquals(want.sessionBus?.broadcast, test.sessionBus?.broadcast, "sessionBus.broadcast")
|
||||
assertEquals(want.sessionBus?.log ?: false, test.sessionBus?.log, "sessionBus.log")
|
||||
assertEquals(want.sessionBus?.filter, test.sessionBus?.filter, "sessionBus.filter")
|
||||
|
||||
assertEquals(want.systemBus?.see, test.systemBus?.see, "systemBus.see")
|
||||
assertEquals(want.systemBus?.talk, test.systemBus?.talk, "systemBus.talk")
|
||||
assertEquals(want.systemBus?.own, test.systemBus?.own, "systemBus.own")
|
||||
assertEquals(want.systemBus?.broadcast, test.systemBus?.broadcast, "systemBus.broadcast")
|
||||
assertEquals(want.systemBus?.log ?: false, test.systemBus?.log, "systemBus.log")
|
||||
assertEquals(want.systemBus?.filter, test.systemBus?.filter, "systemBus.filter")
|
||||
|
||||
assertEquals(want.directWayland ?: false, test.directWayland, "directWayland")
|
||||
assertEquals(want.username, test.username, "username")
|
||||
assertEquals(want.shell, test.shell, "shell")
|
||||
assertEquals(want.data, test.data, "data")
|
||||
assertEquals(want.dir, test.dir, "dir")
|
||||
assertEquals(want.extraPerms, test.extraPerms, "extraPerms")
|
||||
|
||||
assertEquals(want.identity, test.identity, "identity")
|
||||
assertEquals(want.groups, test.groups, "groups")
|
||||
|
||||
assertEquals(want.container.waitDelay, test.container.waitDelay, "waitDelay")
|
||||
assertEquals(want.container.seccompFlags, test.container.seccompFlags, "seccompFlags")
|
||||
assertEquals(want.container.seccompPresets, test.container.seccompPresets, "seccompPresets")
|
||||
assertEquals(want.container.seccompCompat, test.container.seccompCompat, "seccompCompat")
|
||||
|
||||
assertEquals(want.container.devel, test.container.devel, "devel")
|
||||
assertEquals(want.container.userns, test.container.userns, "userns")
|
||||
assertEquals(want.container.net, test.container.net, "net")
|
||||
assertEquals(want.container.tty, test.container.tty, "tty")
|
||||
assertEquals(want.container.multiarch, test.container.multiarch, "multiarch")
|
||||
|
||||
assertEquals(want.container.env, test.container.env, "env")
|
||||
assertEquals(want.container.mapRealUid, test.container.mapRealUid, "mapRealUid")
|
||||
|
||||
assertEquals(want.container.device, test.container.device, "device")
|
||||
assertEquals(want.container.filesystem.size, test.container.filesystem.size, "filesystem.size")
|
||||
for(i in 0 until want.container.filesystem.size) {
|
||||
val w = want.container.filesystem[i]
|
||||
val t = test.container.filesystem[i]
|
||||
|
||||
assertEquals(w.src, t.src, "filesystem[$i].src")
|
||||
assertEquals(w.dst, t.dst, "filesystem[$i].dst")
|
||||
assertEquals(w.write, t.write, "filesystem[$i].write")
|
||||
assertEquals(w.device, t.device, "filesystem[$i].device")
|
||||
assertEquals(w.must, t.must, "filesystem[$i].must")
|
||||
}
|
||||
assertEquals(want.container.link, test.container.link, "link")
|
||||
assertEquals(want.container.autoRoot, test.container.autoRoot, "autoroot")
|
||||
|
||||
assertEquals(want.container.etc, test.container.etc, "etc")
|
||||
|
||||
assertEquals(want.container.autoEtc, test.container.autoEtc, "autoEtc")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHakureiConfigSerialization() {
|
||||
val json = format.encodeToString(testConfig)
|
||||
val test = format.decodeFromString<HakureiConfig>(json)
|
||||
assertEquals(testConfig, test)
|
||||
val want = format.decodeFromString<HakureiConfig>(want)
|
||||
|
||||
compare(want, test)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEnablements() {
|
||||
val enablement = HakureiConfig.Enablement.enablements(Wayland, X11, DBus, Pulse)
|
||||
assertEquals(enablement, 0b1111)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSeccompFilterPresets() {
|
||||
val presets = HakureiConfig.SeccompFilterPreset.filterPresets(Ext, DenyNS, DenyTTY, DenyDevel, Linux32)
|
||||
assertEquals(presets, 0b11111)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExportFlags() {
|
||||
val flags = HakureiConfig.HakureiExportFlag.exportFlags(Multiarch, CAN, Bluetooth)
|
||||
assertEquals(flags, 0b111)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRootFlags() {
|
||||
val flags = HakureiConfig.RootFlag.rootFlags(Optional, Writable, Device)
|
||||
assertEquals(flags, 0b111)
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
kotlin {
|
||||
jvm()
|
||||
val nativeTarget = if(System.getProperty("os.arch") == "aarch64") {
|
||||
linuxArm64("native")
|
||||
} else {
|
||||
linuxX64("native")
|
||||
}
|
||||
nativeTarget.binaries {
|
||||
executable()
|
||||
}
|
||||
sourceSets {
|
||||
nativeMain.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
}
|
||||
nativeTest.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
jvmMain.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
}
|
||||
jvmTest.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package app.hakurei.planterette.api
|
||||
|
||||
interface Task {
|
||||
val execute: String
|
||||
}
|
||||
class AptInstallTask(val packages: List<String>) : Task {
|
||||
private fun getPackageList(): String {
|
||||
var string = ""
|
||||
packages.forEach { p ->
|
||||
string += "$p "
|
||||
}
|
||||
return string
|
||||
}
|
||||
override val execute: String
|
||||
get() = "sudo apt install ${getPackageList()}"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
kotlin {
|
||||
val nativeTarget = if(System.getProperty("os.arch") == "aarch64") {
|
||||
linuxArm64("native")
|
||||
} else {
|
||||
linuxX64("native")
|
||||
}
|
||||
nativeTarget.binaries {
|
||||
executable()
|
||||
}
|
||||
sourceSets {
|
||||
nativeMain.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
}
|
||||
nativeTest.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
kotlin {
|
||||
val nativeTarget = if(System.getProperty("os.arch") == "aarch64") {
|
||||
linuxArm64("native")
|
||||
} else {
|
||||
linuxX64("native")
|
||||
}
|
||||
nativeTarget.binaries {
|
||||
executable()
|
||||
}
|
||||
sourceSets {
|
||||
nativeMain.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
}
|
||||
nativeTest.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
kotlin {
|
||||
val nativeTarget = if(System.getProperty("os.arch") == "aarch64") {
|
||||
linuxArm64("native")
|
||||
} else {
|
||||
linuxX64("native")
|
||||
}
|
||||
nativeTarget.binaries {
|
||||
executable()
|
||||
}
|
||||
sourceSets {
|
||||
nativeMain.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
}
|
||||
nativeTest.dependencies {
|
||||
implementation(project(":libplt"))
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,4 @@
|
||||
dependencyResolutionManagement {
|
||||
@Suppress("UnstableApiUsage")
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||
}
|
||||
|
||||
rootProject.name = "planterette"
|
||||
|
||||
include("plt")
|
||||
include("libplt")
|
||||
include("plt-build")
|
||||
include("plt-fetch")
|
||||
include("plt-pkg")
|
||||
5
src/main/go/main.go
Normal file
5
src/main/go/main.go
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
||||
10
src/main/go/main_test.go
Normal file
10
src/main/go/main_test.go
Normal file
@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
_ "testing"
|
||||
)
|
||||
|
||||
func TestHelloWorld(t *testing.T) {
|
||||
|
||||
}
|
||||
2
src/main/kotlin/moe/rosa/planterette/Main.kt
Normal file
2
src/main/kotlin/moe/rosa/planterette/Main.kt
Normal file
@ -0,0 +1,2 @@
|
||||
package moe.rosa.planterette
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
package moe.rosa.planterette
|
||||
|
||||
import moe.rosa.planterette.hakurei.HakureiConfig
|
||||
|
||||
data class PlanteretteConfig(var hakurei: HakureiConfig?)
|
||||
12
src/main/kotlin/moe/rosa/planterette/dsl/DSL.kt
Normal file
12
src/main/kotlin/moe/rosa/planterette/dsl/DSL.kt
Normal file
@ -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)
|
||||
}
|
||||
392
src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt
Normal file
392
src/main/kotlin/moe/rosa/planterette/dsl/HakureiDSL.kt
Normal file
@ -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<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
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
85
src/main/kotlin/moe/rosa/planterette/hakurei/Filesystem.kt
Normal file
85
src/main/kotlin/moe/rosa/planterette/hakurei/Filesystem.kt
Normal file
@ -0,0 +1,85 @@
|
||||
package moe.rosa.planterette.hakurei
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* AbsolutePath 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)
|
||||
}
|
||||
}
|
||||
operator fun plus(other: String): AbsolutePath {
|
||||
return AbsolutePath(pathname + other)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AbsolutePathException is returned by @see AbsolutePath() and holds the invalid pathname.
|
||||
*/
|
||||
data class AbsolutePathException(val pathname: String) : IllegalArgumentException("Path $pathname is not absolute")
|
||||
|
||||
@Serializable sealed interface FilesystemConfig
|
||||
|
||||
@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
|
||||
|
||||
@Serializable
|
||||
@SerialName("ephemeral")
|
||||
data class FSEphemeral(
|
||||
@SerialName("dst") val target: AbsolutePath,
|
||||
val write: Boolean,
|
||||
val size: Int? = null,
|
||||
val perm: Int,
|
||||
) : FilesystemConfig
|
||||
|
||||
@Serializable
|
||||
@SerialName("link")
|
||||
data class FSLink(
|
||||
@SerialName("dst") val target: AbsolutePath,
|
||||
val linkname: String,
|
||||
val dereference: Boolean,
|
||||
) : FilesystemConfig
|
||||
|
||||
@Serializable
|
||||
@SerialName("overlay")
|
||||
data class FSOverlay(
|
||||
@SerialName("dst") val target: AbsolutePath,
|
||||
val lower: List<AbsolutePath>,
|
||||
val upper: AbsolutePath,
|
||||
val work: AbsolutePath,
|
||||
) : FilesystemConfig
|
||||
91
src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt
Normal file
91
src/main/kotlin/moe/rosa/planterette/hakurei/Hakurei.kt
Normal file
@ -0,0 +1,91 @@
|
||||
package moe.rosa.planterette.hakurei
|
||||
|
||||
import kotlinx.serialization.*
|
||||
|
||||
@Serializable
|
||||
data class HakureiConfig(
|
||||
var id: String? = null,
|
||||
var path: AbsolutePath? = null,
|
||||
var args: List<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,
|
||||
var username: String? = null,
|
||||
var shell: AbsolutePath? = null,
|
||||
var home: AbsolutePath? = null,
|
||||
|
||||
@SerialName("extra_perms") var extraPerms: List<ExtraPermsConfig>? = null,
|
||||
var identity: Int? = null,
|
||||
var groups: List<String>? = null,
|
||||
|
||||
var container: ContainerConfig? = null,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Enablements(
|
||||
var wayland: Boolean? = null,
|
||||
var x11: Boolean? = null,
|
||||
var dbus: Boolean? = null,
|
||||
var pulse: Boolean? = null,
|
||||
)
|
||||
111
src/test/kotlin/DSLTest.kt
Normal file
111
src/test/kotlin/DSLTest.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
194
src/test/kotlin/HakureiTest.kt
Normal file
194
src/test/kotlin/HakureiTest.kt
Normal file
@ -0,0 +1,194 @@
|
||||
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",
|
||||
path = AbsolutePath("/run/current-system/sw/bin/chromium"),
|
||||
args = listOf(
|
||||
"chromium",
|
||||
"--ignore-gpu-blocklist",
|
||||
"--disable-smooth-scrolling",
|
||||
"--enable-features=UseOzonePlatform",
|
||||
"--ozone-platform=wayland"
|
||||
),
|
||||
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
|
||||
),
|
||||
username = "chronos",
|
||||
shell = AbsolutePath("/run/current-system/sw/bin/zsh"),
|
||||
home = AbsolutePath("/data/data/org.chromium.Chromium"),
|
||||
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("/mnt-root/nix/.ro-store")
|
||||
),
|
||||
upper = AbsolutePath("/mnt-root/nix/.rw-store/upper"),
|
||||
work = AbsolutePath("/mnt-root/nix/.rw-store/work")
|
||||
),
|
||||
FSBind(
|
||||
source = AbsolutePath("/nix/store")
|
||||
),
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
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())
|
||||
}
|
||||
}
|
||||
0
src/test/resources/ChromiumExample.kts
Normal file
0
src/test/resources/ChromiumExample.kts
Normal file
Loading…
x
Reference in New Issue
Block a user