From 323dcb282082f1aa6cc66a9100be0fadb077714c Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 17 Jun 2026 01:29:47 +0900 Subject: [PATCH] cmd/app: high-level app configuration syntax This replaces the nixos module. Signed-off-by: Ophestra --- cmd/app/app.go | 262 ++++++++++++++++++++++++++++++++++++++++++++ cmd/app/app_test.go | 152 +++++++++++++++++++++++++ hst/container.go | 5 + hst/dbus.go | 5 + 4 files changed, 424 insertions(+) create mode 100644 cmd/app/app.go create mode 100644 cmd/app/app_test.go 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 {