cmd/app: high-level app configuration syntax
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 4m8s
Test / Hakurei (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 7m0s
Test / Flake checks (push) Successful in 1m23s

This replaces the nixos module.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-06-17 01:29:47 +09:00
parent f46a0370a7
commit 323dcb2820
4 changed files with 424 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
package main
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
// parsePair parses a NUL-delimited quoted paths pair.
func parsePair(s string) (source, target *check.Absolute, err error) {
var p string
if p, err = strconv.Unquote(s); err != nil {
return
}
_source, _target, ok := strings.Cut(p, "\x00")
if source, err = check.NewAbs(_source); err != nil {
return
}
if !ok {
return
}
target, err = check.NewAbs(_target)
return
}
// parse decodes a high-level configuration stream and returns its
// corresponding [hst.Config].
func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) {
shell := fhs.AbsRoot.Append("bin", "zsh")
home := hst.AbsPrivateTmp.Append("home")
c := hst.Config{
ID: id,
Enablements: new(hst.Enablements),
SessionBus: &hst.BusConfig{
Own: []string{
id + ".*",
"org.mpris.MediaPlayer2." + id + ".*",
},
Filter: true,
},
SystemBus: &hst.BusConfig{Filter: true},
Container: &hst.ContainerConfig{
Env: make(map[string]string),
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
base.Append("template", "initial"),
base.Append("template", "upper"),
},
}},
{FilesystemConfig: &hst.FSBind{
Target: home,
Source: base.Append("state", id),
Write: true,
Ensure: true,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsVar.Append("tmp"),
Write: true,
Perm: 01777,
}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
},
Username: "chronos",
Shell: shell,
Home: home,
Path: shell,
Args: []string{"zsh", "-c"},
},
}
s := bufio.NewScanner(r)
scanOnce := func() error {
if s.Scan() {
return nil
}
if err := s.Err(); err != nil {
return err
}
return io.ErrUnexpectedEOF
}
if err := scanOnce(); err != nil {
return nil, err
}
if v, err := strconv.Atoi(s.Text()); err != nil {
return nil, err
} else {
c.Identity = v
}
if err := scanOnce(); err != nil {
return nil, err
}
c.Container.Args = append(c.Container.Args, s.Text())
var flagGPU, flagSystemBus bool
flags := map[string]*bool{
"gpu": &flagGPU,
"system_bus": &flagSystemBus,
}
for s.Scan() {
key, value, ok := strings.Cut(s.Text(), " ")
if key != "" && key[0] == ';' {
continue
}
if !ok {
if key == "" {
continue
}
var p *bool
if p, ok = flags[key]; ok {
*p = true
continue
}
switch key {
case "wayland":
*c.Enablements |= hst.EWayland
case "x11":
*c.Enablements |= hst.EX11
case "dbus":
*c.Enablements |= hst.EDBus
case "pipewire":
*c.Enablements |= hst.EPipeWire
case "multiarch":
c.Container.Flags |= hst.FMultiarch
case "devel":
c.Container.Flags |= hst.FDevel
case "userns":
c.Container.Flags |= hst.FUserns
case "net":
c.Container.Flags |= hst.FHostNet
case "abstract":
c.Container.Flags |= hst.FHostAbstract
case "tty":
c.Container.Flags |= hst.FTty
case "mapuid":
c.Container.Flags |= hst.FMapRealUID
case "device":
c.Container.Flags |= hst.FDevice
case "share_runtime":
c.Container.Flags |= hst.FShareRuntime
case "share_tmpdir":
c.Container.Flags |= hst.FShareTmpdir
default:
return nil, fmt.Errorf("invalid flag %q", key)
}
continue
}
switch key {
case "group":
c.Groups = append(c.Groups, value)
continue
case "env":
if key, value, ok = strings.Cut(value, "="); !ok {
return nil, fmt.Errorf("invalid environment %q", key)
}
c.Container.Env[key] = value
continue
case "ro":
source, target, err := parsePair(value)
if err != nil {
return nil, err
}
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: target,
Source: source,
}},
)
continue
case "rw":
source, target, err := parsePair(value)
if err != nil {
return nil, err
}
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: target,
Source: source,
Write: true,
}},
)
continue
case "own":
c.SessionBus.Own = append(c.SessionBus.Own, value)
continue
case "own_system":
c.SystemBus.Own = append(c.SystemBus.Own, value)
continue
case "talk":
c.SessionBus.Talk = append(c.SessionBus.Talk, value)
continue
case "talk_system":
c.SystemBus.Talk = append(c.SystemBus.Talk, value)
continue
default:
return nil, fmt.Errorf("invalid key %q", key)
}
}
if err := s.Err(); err != nil {
return nil, err
}
if flagGPU {
c.Container.Filesystem = append(c.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
}...)
}
if !flagSystemBus {
c.SystemBus = nil
}
if c.Container.Flags&hst.FShareTmpdir == 0 {
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 01777,
}},
)
}
return &c, nil
}
+152
View File
@@ -0,0 +1,152 @@
package main
import (
"reflect"
"strings"
"testing"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
func TestParse(t *testing.T) {
t.Parallel()
base := fhs.AbsProc.Append("nonexistent")
testCases := []struct {
name string
data string
want *hst.Config
err error
}{
{"com.discordapp.Discord", `8
exec Discord --ozone-platform-hint=wayland
gpu
wayland
dbus
system_bus
pipewire
userns
net
mapuid
share_runtime
share_tmpdir
group media_rw
env ELECTRON_TRASH=gio
rw "/sdcard"
; remove before reusing
ro "/bin\x00/.hakurei/bin"
talk org.kde.StatusNotifierWatcher
talk com.canonical.AppMenu.Registrar
talk com.canonical.indicator.application
talk com.canonical.Unity
`, &hst.Config{
Identity: 8,
ID: "com.discordapp.Discord",
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire),
Groups: []string{"media_rw"},
SessionBus: &hst.BusConfig{
Talk: []string{
"org.kde.StatusNotifierWatcher",
"com.canonical.AppMenu.Registrar",
"com.canonical.indicator.application",
"com.canonical.Unity",
},
Own: []string{
"com.discordapp.Discord.*",
"org.mpris.MediaPlayer2.com.discordapp.Discord.*",
},
Filter: true,
},
SystemBus: &hst.BusConfig{Filter: true},
Container: &hst.ContainerConfig{
Env: map[string]string{
"ELECTRON_TRASH": "gio",
},
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
base.Append("template", "initial"),
base.Append("template", "upper"),
},
}},
{FilesystemConfig: &hst.FSBind{
Target: hst.AbsPrivateTmp.Append("home"),
Source: base.Append("state", "com.discordapp.Discord"),
Write: true,
Ensure: true,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsVar.Append("tmp"),
Write: true,
Perm: 01777,
}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
{FilesystemConfig: &hst.FSBind{
Source: check.MustAbs("/sdcard"),
Write: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: check.MustAbs("/.hakurei/bin"),
Source: check.MustAbs("/bin"),
}},
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
},
Username: "chronos",
Shell: fhs.AbsRoot.Append("bin", "zsh"),
Home: hst.AbsPrivateTmp.Append("home"),
Path: fhs.AbsRoot.Append("bin", "zsh"),
Args: []string{
"zsh", "-c",
"exec Discord --ozone-platform-hint=wayland",
},
Flags: hst.FUserns | hst.FHostNet | hst.FMapRealUID |
hst.FShareRuntime | hst.FShareTmpdir,
},
}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := parse(
tc.name,
base,
strings.NewReader(tc.data),
)
if !reflect.DeepEqual(err, tc.err) {
t.Errorf("parse: error = %v, want %v", err, tc.err)
}
if err != nil {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parse: %#v, want %#v", got, tc.want)
}
})
}
}
+5
View File
@@ -2,6 +2,7 @@ package hst
import (
"encoding/json"
"fmt"
"strings"
"syscall"
"time"
@@ -161,6 +162,10 @@ type ContainerConfig struct {
Flags Flags `json:"-"`
}
func (c *ContainerConfig) GoString() string {
return fmt.Sprintf("&%#v", *c)
}
// ContainerConfigF is [ContainerConfig] stripped of its methods.
//
// The [ContainerConfig.Flags] field does not survive a [json] round trip.
+5
View File
@@ -1,6 +1,7 @@
package hst
import (
"fmt"
"strconv"
"strings"
)
@@ -61,6 +62,10 @@ type BusConfig struct {
Filter bool `json:"filter"`
}
func (c *BusConfig) GoString() string {
return fmt.Sprintf("&%#v", *c)
}
// Interfaces iterates over all interface strings specified in [BusConfig].
func (c *BusConfig) Interfaces(yield func(string) bool) {
if c == nil {