aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-06-17 01:29:47 +0900
committerOphestra <cat@gensokyo.uk>2026-06-17 01:50:53 +0900
commit323dcb282082f1aa6cc66a9100be0fadb077714c (patch)
tree16896bb84c470a0fb370d6e7eee22888a08b164c
parentf46a0370a723f3920fc3be5edc8ca630d9039dc5 (diff)
cmd/app: high-level app configuration syntax
This replaces the nixos module. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--cmd/app/app.go262
-rw-r--r--cmd/app/app_test.go152
-rw-r--r--hst/container.go5
-rw-r--r--hst/dbus.go5
4 files changed, 424 insertions, 0 deletions
diff --git a/cmd/app/app.go b/cmd/app/app.go
new file mode 100644
index 00000000..74ff9ad4
--- /dev/null
+++ b/cmd/app/app.go
@@ -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
+}
diff --git a/cmd/app/app_test.go b/cmd/app/app_test.go
new file mode 100644
index 00000000..df17f781
--- /dev/null
+++ b/cmd/app/app_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/hst/container.go b/hst/container.go
index 15a5d221..f985f393 100644
--- a/hst/container.go
+++ b/hst/container.go
@@ -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.
diff --git a/hst/dbus.go b/hst/dbus.go
index 5c4620cc..7b8a3585 100644
--- a/hst/dbus.go
+++ b/hst/dbus.go
@@ -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 {