Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -12,27 +12,35 @@ import (
|
||||
// every desktop module that carries it.
|
||||
|
||||
type manifest struct {
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Requires []string `json:"requires"`
|
||||
Claims []claim `json:"claims"`
|
||||
Seats []any `json:"seats"`
|
||||
Tools []string `json:"tools"`
|
||||
Settings map[string]setting `json:"settings"`
|
||||
Environment *environment `json:"environment"`
|
||||
Shell []shellCode `json:"shell"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Module string `json:"module"`
|
||||
Version string `json:"version"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Requires []string `json:"requires"`
|
||||
Claims []claim `json:"claims"`
|
||||
Seats []any `json:"seats"`
|
||||
Tools []string `json:"tools"`
|
||||
Config map[string]config `json:"config"`
|
||||
Environment *environment `json:"environment"`
|
||||
Shell []shellCode `json:"shell"`
|
||||
Resources []map[string]any `json:"resources"`
|
||||
Build struct {
|
||||
Artifacts []map[string]any `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
}
|
||||
|
||||
// setting is one key's default, as the definition gives it (novox/hq ADR 0262).
|
||||
type setting struct {
|
||||
Kind string `json:"kind"`
|
||||
Default any `json:"default"`
|
||||
Why string `json:"why"`
|
||||
// config is one config value as the definition declares it (novox/hq ADR 0348 §4).
|
||||
type config struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Enum []any `json:"enum"`
|
||||
Minimum *float64 `json:"minimum"`
|
||||
Maximum *float64 `json:"maximum"`
|
||||
Pattern string `json:"pattern"`
|
||||
Trusted bool `json:"trusted"`
|
||||
Predefined string `json:"predefined"`
|
||||
Default any `json:"default"`
|
||||
Required bool `json:"required"`
|
||||
Runtime bool `json:"runtime"`
|
||||
}
|
||||
|
||||
type claim struct {
|
||||
@@ -91,26 +99,66 @@ func (m manifest) packages() (present, absent []string) {
|
||||
return present, absent
|
||||
}
|
||||
|
||||
// sameAsSource checks that a file resource's content is byte for byte the module's source file, so
|
||||
// the readable file in the repository is what the machine gets.
|
||||
func (m manifest) sameAsSource(t *testing.T, id, source string) {
|
||||
// fromTemplate checks that a file resource takes its text from its template file in the module's
|
||||
// repository (novox/hq ADR 0349 §2): it names the template, carries no text of its own beside it, and the
|
||||
// template is there. The readable file in the repository is the one place the machine's text is written.
|
||||
func (m manifest) fromTemplate(t *testing.T, id, template string) {
|
||||
t.Helper()
|
||||
want, err := os.ReadFile(filepath.Join("..", "..", source))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := m.resource(t, id)
|
||||
if r["type"] != "file" {
|
||||
t.Fatalf("%s is a %v, not a file", id, r["type"])
|
||||
}
|
||||
if got, _ := r["content"].(string); got != string(want) {
|
||||
t.Fatalf("resource %s's content is not %s: edit the source and copy it into module.json", id, source)
|
||||
if r["template"] != template {
|
||||
t.Fatalf("resource %s does not take its text from %s: template %v", id, template, r["template"])
|
||||
}
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Fatalf("resource %s keeps its text inline beside its template: a file's text is in one place", id)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("..", "..", template)); err != nil {
|
||||
t.Fatalf("resource %s names %s: %v", id, template, err)
|
||||
}
|
||||
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
|
||||
t.Fatalf("%s under the home is the account's", id)
|
||||
}
|
||||
}
|
||||
|
||||
// everyFileIsATemplate checks that no file resource carries its text inline, and that the build packs
|
||||
// files/ into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
||||
func (m manifest) everyFileIsATemplate(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, r := range m.Resources {
|
||||
if _, inline := r["content"]; inline {
|
||||
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
||||
}
|
||||
}
|
||||
packs := 0
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a["kind"] == "archive" && a["from"] == "files" {
|
||||
packs++
|
||||
if a["name"] != "files" {
|
||||
t.Errorf("the templates archive is named %v, not files", a["name"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if packs != 1 {
|
||||
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
||||
}
|
||||
}
|
||||
|
||||
// text is the text a file resource places on the machine: its template file's.
|
||||
func (m manifest) text(t *testing.T, id string) string {
|
||||
t.Helper()
|
||||
name, _ := m.resource(t, id)["template"].(string)
|
||||
if name == "" {
|
||||
t.Fatalf("resource %s names no template", id)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
|
||||
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
|
||||
func checkTheToolsAgree(t *testing.T, m manifest) {
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestItClaimsTheCompositorSeatAndRequiresTheXDisplay(t *testing.T) {
|
||||
|
||||
func TestItOwnsItsConfigurationAsWrittenInTheModule(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
m.sameAsSource(t, "configuration", "files/picom.conf")
|
||||
m.fromTemplate(t, "configuration", "files/picom.conf")
|
||||
if p := m.resource(t, "configuration")["path"]; p != "${machine:account-home}/.config/picom/picom.conf" {
|
||||
t.Fatalf("path: %v", p)
|
||||
}
|
||||
@@ -62,16 +62,19 @@ func TestTheToolsAgreeWithTheManifest(t *testing.T) {
|
||||
|
||||
// The backend and damage tracking are each machine's (novox/hq research 037: the backend depends on the
|
||||
// machine's graphics driver; issue 345: a window left unpainted under glx is the first suspect). Both
|
||||
// are settings whose defaults are what every machine ran before, so the file filled with them is today's.
|
||||
func TestTheBackendAndDamageTrackingAreSettingsWithTodaysDefaults(t *testing.T) {
|
||||
// are config values whose defaults are what every machine ran before, so the file filled with them is today's
|
||||
// (novox/hq ADR 0348 §4): the backend a string of picom's backends, damage tracking a boolean, both trusted
|
||||
// as the file is.
|
||||
func TestTheBackendAndDamageTrackingAreConfigValuesWithTodaysDefaults(t *testing.T) {
|
||||
m := readManifest(t)
|
||||
b, d := m.Settings["backend"], m.Settings["use-damage"]
|
||||
if b.Kind != "preference" || b.Default != "glx" || b.Why == "" || d.Kind != "preference" || d.Default != true || d.Why == "" {
|
||||
t.Fatalf("settings: %+v", m.Settings)
|
||||
b, d := m.Config["backend"], m.Config["use-damage"]
|
||||
if b.Type != "string" || b.Default != "glx" || b.Description == "" || !b.Trusted || len(b.Enum) == 0 ||
|
||||
d.Type != "boolean" || d.Default != true || d.Description == "" || !d.Trusted {
|
||||
t.Fatalf("config: %+v", m.Config)
|
||||
}
|
||||
r := m.resource(t, "configuration")
|
||||
c := r["content"].(string)
|
||||
for _, want := range []string{`backend = "${setting:backend}";`, "use-damage = ${setting:use-damage};"} {
|
||||
c := m.text(t, "configuration")
|
||||
for _, want := range []string{`backend = "${config:backend}";`, "use-damage = ${config:use-damage};"} {
|
||||
if !strings.Contains(c, want) {
|
||||
t.Errorf("the file asks for %q", want)
|
||||
}
|
||||
@@ -88,7 +91,12 @@ func TestTheBackendAndDamageTrackingAreSettingsWithTodaysDefaults(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// withDefaults fills the file's settings with their defaults, as a machine without its own values gets it.
|
||||
// withDefaults fills the file's config values with their defaults, as a machine without its own values gets it.
|
||||
func withDefaults(c string) string {
|
||||
return strings.NewReplacer("${setting:backend}", "glx", "${setting:use-damage}", "true").Replace(c)
|
||||
return strings.NewReplacer("${config:backend}", "glx", "${config:use-damage}", "true").Replace(c)
|
||||
}
|
||||
|
||||
// No file resource carries its text inline (novox/hq ADR 0349 §2).
|
||||
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
||||
readManifest(t).everyFileIsATemplate(t)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
# machine ran before: glx, and picom's own default of repainting only what changed. xrender, or
|
||||
# use-damage false (repaint the whole screen each frame), is what to try on a machine where a window is
|
||||
# left unpainted or stale.
|
||||
backend = "${setting:backend}";
|
||||
use-damage = ${setting:use-damage};
|
||||
backend = "${config:backend}";
|
||||
use-damage = ${config:use-damage};
|
||||
vsync = true;
|
||||
|
||||
fading = true;
|
||||
|
||||
+25
-12
@@ -13,18 +13,26 @@
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"backend": {
|
||||
"kind": "preference",
|
||||
"default": "glx",
|
||||
"why": "glx draws with the graphics card and has served every machine so far; xrender draws on the CPU and is what to try where glx leaves a window unpainted or stale on the machine's driver (novox/hq research 037, issue 345)"
|
||||
"config": {
|
||||
"backend": {
|
||||
"type": "string",
|
||||
"description": "How picom draws: glx with the graphics card, which has served every machine so far, or xrender on the CPU where glx leaves a window unpainted or stale on the machine's driver (novox/hq research 037, issue 345); xr_glx_hybrid and egl are picom's other backends.",
|
||||
"enum": [
|
||||
"glx",
|
||||
"xrender",
|
||||
"xr_glx_hybrid",
|
||||
"egl"
|
||||
],
|
||||
"trusted": true,
|
||||
"default": "glx"
|
||||
},
|
||||
"use-damage": {
|
||||
"type": "boolean",
|
||||
"description": "Whether picom repaints only what changed (true, picom's own default) or the whole screen each frame (false), which costs work and cures a window left stale or unpainted on some drivers (novox/hq issue 345).",
|
||||
"trusted": true,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"use-damage": {
|
||||
"kind": "preference",
|
||||
"default": true,
|
||||
"why": "true repaints only what changed, picom's own default; false repaints the whole screen each frame, which costs work and cures a window left stale or unpainted on some drivers (novox/hq issue 345)"
|
||||
}
|
||||
},
|
||||
"tools": [
|
||||
"picom_restart",
|
||||
"picom_rules",
|
||||
@@ -55,7 +63,7 @@
|
||||
"path": "${machine:account-home}/.config/picom/picom.conf",
|
||||
"owner": "${machine:account}",
|
||||
"mode": "0644",
|
||||
"content": "# picom, the X compositor (module picom, novox/hq ADR 0208). Owned by the mesh: this file is\n# replaced at every push. Adopted from the two workstations' file of 2026-10-04 and moved to\n# picom's window rules, which supersede opacity-rule, inactive-opacity and inactive-dim.\n\n# The backend and damage tracking are each machine's settings (backend, use-damage; novox/hq research\n# 037, issue 345): the right ones depend on the machine's graphics driver. The defaults are what every\n# machine ran before: glx, and picom's own default of repainting only what changed. xrender, or\n# use-damage false (repaint the whole screen each frame), is what to try on a machine where a window is\n# left unpainted or stale.\nbackend = \"${setting:backend}\";\nuse-damage = ${setting:use-damage};\nvsync = true;\n\nfading = true;\nfade-in-step = 0.05;\nfade-out-step = 0.05;\n\n# Tiled windows hide their shadows and corners, so neither is drawn.\nshadow = false;\ncorner-radius = 0;\nblur-method = \"none\";\n\n# \"Focused\" is i3's _NET_ACTIVE_WINDOW, not X focus events: under i3 those do not reliably reach\n# the toplevel picom tracks, and a terminal then never lifts to its focused opacity.\nuse-ewmh-active-win = true;\n\n# Window rules are applied in order, and a later match wins. Only terminals are translucent:\n# nothing else on screen (browsers, video, games) is touched. A terminal is matched by its class;\n# the node's terminal emulator today is xterm (class XTerm). A window's own opacity property is\n# used wherever no rule sets one, which is what the window-opacity tool sets.\nrules = (\n { match = \"window_type = 'tooltip'\"; fade = false; opacity = 0.95; },\n { match = \"window_type = 'dock' || window_type = 'desktop'\"; fade = false; },\n { match = \"class_g = 'XTerm' && focused\"; opacity = 0.95; },\n { match = \"class_g = 'XTerm' && !focused\"; opacity = 0.75; },\n # A full-screen window is opaque, a terminal included: a video or a game is never see-through.\n { match = \"fullscreen\"; opacity = 1; }\n);\n"
|
||||
"template": "files/picom.conf"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
@@ -70,6 +78,11 @@
|
||||
"loads": [
|
||||
"picom-tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user