aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-09-17 02:30:10 +0900
committerOphestra <cat@gensokyo.uk>2026-09-17 02:30:10 +0900
commit4078289cdadb4def6ae28428ce8bf3c7907f66f1 (patch)
treefd7c9dd9192cc4bb9aeb94c1efb7411687535231 /cmd
parent3fbdf138bb7ce07319d2c9e3d89b3a3892e139d2 (diff)
cmd/app: alternative merged template layout
This implements an optional, alternative template layout to accommodate ZFS and equivalent setups where each template occupies a different filesystem. In such a setup, the default, separate workdir is unusable as overlayfs requires the workdir to reside on the same filesystem as the writable upperdir. The "merge" build tag enables this alternative behaviour. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/app/app.go26
-rw-r--r--cmd/app/app_merged.go15
-rw-r--r--cmd/app/app_sep.go15
-rw-r--r--cmd/app/app_test.go10
-rw-r--r--cmd/app/doc.go13
-rw-r--r--cmd/app/lock.go19
-rw-r--r--cmd/app/main.go35
7 files changed, 94 insertions, 39 deletions
diff --git a/cmd/app/app.go b/cmd/app/app.go
index 9679413f..ca809334 100644
--- a/cmd/app/app.go
+++ b/cmd/app/app.go
@@ -14,6 +14,24 @@ import (
"hakurei.app/hst"
)
+// base is the root of the persistent state directory.
+type base check.Absolute
+
+// append calls filepath.Join with b as the first element.
+func (b *base) append(elem ...string) *check.Absolute {
+ return (*check.Absolute)(b).Append(elem...)
+}
+
+// initial returns the pathname of the bottom layer.
+func (b *base) initial() *check.Absolute {
+ return b.append("initial")
+}
+
+// state returns the pathname of the home directory for id.
+func (b *base) state(id string) *check.Absolute {
+ return b.append("state", id)
+}
+
// parsePair parses a NUL-delimited quoted paths pair.
func parsePair(s string) (source, target *check.Absolute, err error) {
var p string
@@ -35,7 +53,7 @@ func parsePair(s string) (source, target *check.Absolute, err error) {
// corresponding [hst.Config].
func parse(
id string,
- base *check.Absolute,
+ base *base,
r io.Reader,
templateP *string,
) (*hst.Config, error) {
@@ -44,7 +62,7 @@ func parse(
root := hst.FSOverlay{
Target: fhs.AbsRoot,
- Lower: []*check.Absolute{base.Append("initial")},
+ Lower: []*check.Absolute{base.initial()},
}
c := hst.Config{
ID: id,
@@ -65,7 +83,7 @@ func parse(
{FilesystemConfig: &root},
{FilesystemConfig: &hst.FSBind{
Target: home,
- Source: base.Append("state", id),
+ Source: base.state(id),
Write: true,
Ensure: true,
}},
@@ -110,7 +128,7 @@ func parse(
*templateP = template
}
c.Identity = v
- root.Upper = base.Append("template", template)
+ root.Upper = base.template(template)
}
if err := scanOnce(); err != nil {
diff --git a/cmd/app/app_merged.go b/cmd/app/app_merged.go
new file mode 100644
index 00000000..e0a31f94
--- /dev/null
+++ b/cmd/app/app_merged.go
@@ -0,0 +1,15 @@
+//go:build merge
+
+package main
+
+import "hakurei.app/check"
+
+// template returns the pathname of an upperdir.
+func (b *base) template(template string) *check.Absolute {
+ return b.append("template", template, "upper")
+}
+
+// work returns the pathname of a workdir.
+func (b *base) work(template string) *check.Absolute {
+ return b.append("template", template, "work")
+}
diff --git a/cmd/app/app_sep.go b/cmd/app/app_sep.go
new file mode 100644
index 00000000..9ec98e41
--- /dev/null
+++ b/cmd/app/app_sep.go
@@ -0,0 +1,15 @@
+//go:build !merge
+
+package main
+
+import "hakurei.app/check"
+
+// template returns the pathname of an upperdir.
+func (b *base) template(template string) *check.Absolute {
+ return b.append("template", template)
+}
+
+// work returns the pathname of a workdir.
+func (b *base) work(template string) *check.Absolute {
+ return b.append("work", template)
+}
diff --git a/cmd/app/app_test.go b/cmd/app/app_test.go
index 1aab4759..90c0068e 100644
--- a/cmd/app/app_test.go
+++ b/cmd/app/app_test.go
@@ -13,7 +13,7 @@ import (
func TestParse(t *testing.T) {
t.Parallel()
- base := fhs.AbsProc.Append("nonexistent")
+ b := (*base)(fhs.AbsProc.Append("nonexistent"))
testCases := []struct {
name string
data string
@@ -74,13 +74,13 @@ talk com.canonical.Unity
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
- base.Append("initial"),
+ b.initial(),
},
- Upper: base.Append("template", "nonfree"),
+ Upper: b.template("nonfree"),
}},
{FilesystemConfig: &hst.FSBind{
Target: hst.AbsPrivateTmp.Append("home"),
- Source: base.Append("state", "com.discordapp.Discord"),
+ Source: b.state("com.discordapp.Discord"),
Write: true,
Ensure: true,
}},
@@ -128,7 +128,7 @@ talk com.canonical.Unity
got, err := parse(
tc.name,
- base,
+ b,
strings.NewReader(tc.data),
nil,
)
diff --git a/cmd/app/doc.go b/cmd/app/doc.go
index c25eca6d..ea91315e 100644
--- a/cmd/app/doc.go
+++ b/cmd/app/doc.go
@@ -94,6 +94,14 @@ template, named after the template directory itself, owned by the reserved
user/group. The directory itself and its contents should have the permission
bits set to 0700.
+# Snapshots and cloning
+
+To reduce disk space used by templates, [ZFS clones] or an equivalent can be
+used. In such a setup, where each template occupies a different filesystem, the
+"merge" build tag is required, and the work directory can be omitted during
+directory creation. Instead, the individual template directories contain both
+upper and work directories.
+
# Configuring the base template
This section describes basic setup required for the typical desktop use case.
@@ -101,8 +109,7 @@ Generally, multiple templates are created to mitigate the global nature of
conventional package managers. The setup described in this section applies to
all templates. For convenience, a "base" template should be created, containing
this setup, and all future templates should be copied from the base template.
-To reduce disk space used, [ZFS snapshots] or an equivalent can be used. This
-section assumes the template is named "base".
+This section assumes the template is named "base".
Create an empty directory at $ROSA_APP_PATH/template/base, owned by the reserved
user, with permission bits set to 0755. Create its corresponding work directory
@@ -350,6 +357,6 @@ The following section documents currently available freestanding directives.
[Void Linux rootfs tarball]: https://voidlinux.org/download
[Alpine Linux]: https://alpinelinux.org
[Chimera Linux]: https://chimera-linux.org
-[ZFS snapshots]: https://openzfs.github.io/openzfs-docs/man/v2.4/8/zfs-snapshot.8.html
+[ZFS clones]: https://openzfs.github.io/openzfs-docs/man/v2.4/8/zfs-clone.8.html
*/
package main
diff --git a/cmd/app/lock.go b/cmd/app/lock.go
index 88068381..2a419ea1 100644
--- a/cmd/app/lock.go
+++ b/cmd/app/lock.go
@@ -8,7 +8,6 @@ import (
"strings"
"syscall"
- "hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/env"
@@ -24,15 +23,15 @@ func (e MutationConflictError) Error() string {
}
// informTemplate guards intention of a template or its derivatives.
-func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) {
- mu := lockedfile.MutexAt(base.Append("lock", name).String())
+func informTemplate(b *base, name string, mutable bool) (func() error, error) {
+ mu := lockedfile.MutexAt(b.append("lock", name).String())
if unlock, err := mu.Lock(); err != nil {
return nil, err
} else {
defer unlock()
}
- marker := base.Append("lock", "."+name)
+ marker := b.append("lock", "."+name)
if p, err := os.ReadFile(marker.String()); err == nil {
if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil {
return nil, MutationConflictError(p)
@@ -72,8 +71,8 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err
if !root.Target.Is(fhs.AbsRoot) ||
len(root.Lower) != 1 ||
- !root.Lower[0].Is(base.Append("initial")) ||
- !root.Upper.Is(base.Append("template", name)) ||
+ !root.Lower[0].Is(b.initial()) ||
+ !root.Upper.Is(b.template(name)) ||
root.Work != nil {
continue
}
@@ -102,12 +101,12 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err
}
// acquireTemplate obtains exclusivity of a template.
-func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) {
- return informTemplate(base, name, true)
+func acquireTemplate(b *base, name string) (remove func() error, err error) {
+ return informTemplate(b, name, true)
}
// enterTemplate checks against exclusivity of a template.
-func enterTemplate(base *check.Absolute, name string) error {
- _, err := informTemplate(base, name, false)
+func enterTemplate(b *base, name string) error {
+ _, err := informTemplate(b, name, false)
return err
}
diff --git a/cmd/app/main.go b/cmd/app/main.go
index 5f796cfc..9b51d601 100644
--- a/cmd/app/main.go
+++ b/cmd/app/main.go
@@ -37,7 +37,7 @@ func main() {
flagBase string
flagInsecure bool
- base, template, initial *check.Absolute
+ b *base
)
c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) {
msg.SwapVerbose(flagVerbose)
@@ -45,14 +45,15 @@ func main() {
if flagBase == "" {
flagBase = "state"
}
+
+ var a *check.Absolute
if flagBase, err = filepath.Abs(flagBase); err != nil {
return
- } else if base, err = check.NewAbs(flagBase); err != nil {
+ } else if a, err = check.NewAbs(flagBase); err != nil {
return
}
+ b = (*base)(a)
- template = base.Append("template")
- initial = base.Append("initial")
return
}).Flag(
&flagVerbose,
@@ -77,7 +78,7 @@ func main() {
"enter", "Enter mutable state template",
func(args []string) error {
if len(args) != 1 {
- return list(template, true)
+ return list(b.append("template"), true)
}
config := hst.Config{
@@ -87,9 +88,9 @@ func main() {
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
- Lower: []*check.Absolute{initial},
- Upper: template.Append(args[0]),
- Work: base.Append("work", args[0]),
+ Lower: []*check.Absolute{b.initial()},
+ Upper: b.template(args[0]),
+ Work: b.work(args[0]),
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
@@ -123,7 +124,7 @@ func main() {
config.Container.Home = a
}
- remove, err := acquireTemplate(base, args[0])
+ remove, err := acquireTemplate(b, args[0])
if err != nil {
return err
}
@@ -149,19 +150,19 @@ func main() {
"run", "Start the named application",
func(args []string) error {
if len(args) < 1 {
- return list(base.Append("app"), false)
+ return list(b.append("app"), false)
}
var config *hst.Config
var r io.Reader
- f, err := os.Open(base.Append("app", args[0]).String())
+ f, err := os.Open(b.append("app", args[0]).String())
if err != nil {
return err
}
r = f
var common *os.File
- if common, err = os.Open(base.Append("common").String()); err != nil {
+ if common, err = os.Open(b.append("common").String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
_ = f.Close()
return err
@@ -171,7 +172,7 @@ func main() {
}
var name string
- config, err = parse(args[0], base, r, &name)
+ config, err = parse(args[0], b, r, &name)
if closeErr := f.Close(); err == nil {
err = closeErr
}
@@ -188,7 +189,7 @@ func main() {
config.Container.Args[2] = flagCommand
}
- if err = enterTemplate(base, name); err != nil {
+ if err = enterTemplate(b, name); err != nil {
return err
}
return run(ctx, msg, flagInsecure, config, args[1:]...)
@@ -231,7 +232,7 @@ func main() {
"next", "Find next unused identity",
func([]string) error {
var names []string
- if dents, err := os.ReadDir(base.Append("app").String()); err != nil {
+ if dents, err := os.ReadDir(b.append("app").String()); err != nil {
return err
} else {
names = make([]string, 0, len(dents))
@@ -246,12 +247,12 @@ func main() {
apps := make([]*hst.Config, len(names))
for i, name := range names {
- r, err := os.Open(base.Append("app", name).String())
+ r, err := os.Open(b.append("app", name).String())
if err != nil {
return err
}
- apps[i], err = parse(name, base, r, nil)
+ apps[i], err = parse(name, b, r, nil)
if closeErr := r.Close(); err == nil {
err = closeErr
}