A module that needs code after waking wrote into the service manager's sleep units; it now contributes shell code for a named moment, which derives a dependency on node-power.
576 lines
22 KiB
Go
576 lines
22 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// The account's environment and the login shell's code, composed from the modules a node runs
|
|
// (novox/hq ADR 0203, ADR 0204).
|
|
//
|
|
// **The same shape as the jails.** Every module may contribute — a toolchain its directory on PATH,
|
|
// a version manager a variable naming its home, a prompt the code that loads it — naming no node, no
|
|
// path and no file of the shell's (ADR 0112). The one module holding the matching seat places the
|
|
// result with a placeholder in its own file, and the controller fills it from every module on the
|
|
// node. A node not running a module has none of its contribution, and unassigning one takes its
|
|
// lines away at the next composition.
|
|
//
|
|
// **Two kinds of contribution, kept apart on purpose.** The environment is facts, which the
|
|
// controller writes in two standard formats — POSIX assignment and the service manager's
|
|
// environment.d — so a terminal, a script, the login shell's `execute` and a graphical session all
|
|
// read the same values (ADR 0203). Shell code is not a fact: it is text in one shell's syntax, which
|
|
// the controller sorts into a slot and pastes without reading, as it pastes a jail's stanza (ADR
|
|
// 0204).
|
|
|
|
// EnvironmentSeat and LoginShellSeat are the seats whose holders may place what the modules
|
|
// contributed: the account's environment, and the login shell's code.
|
|
const (
|
|
EnvironmentSeat = "node-environment"
|
|
LoginShellSeat = "node-login-shell"
|
|
// PowerSeat is the seat whose holder places code for the power moments (novox/hq ADR 0211).
|
|
PowerSeat = "node-power"
|
|
)
|
|
|
|
// Where an environment entry on PATH goes: before the account's existing PATH, or after it.
|
|
const (
|
|
PathAtStart = "start"
|
|
PathAtEnd = "end"
|
|
)
|
|
|
|
// Environment is what one module adds to the account's environment (novox/hq ADR 0203).
|
|
type Environment struct {
|
|
// Variables are names and literal values. A value may name the machine's own facts with
|
|
// ${machine:…}, resolved before anything is written, and nothing else that expands.
|
|
Variables map[string]string `json:"variables,omitempty"`
|
|
// Path is entries on the account's PATH, each at its start or its end, in the order declared.
|
|
Path []PathEntry `json:"path,omitempty"`
|
|
}
|
|
|
|
// PathEntry is one directory a module puts on the account's PATH.
|
|
type PathEntry struct {
|
|
Entry string `json:"entry"`
|
|
At string `json:"at"`
|
|
}
|
|
|
|
// ShellCode is one piece of code a module adds to a shell's startup (novox/hq ADR 0204).
|
|
type ShellCode struct {
|
|
// For is the shell the code is written in.
|
|
For string `json:"for"`
|
|
// Slot is where it runs among the other modules' code: first, normal or last. Named rather
|
|
// than numbered, because every contributor would guess a number and a collision says nothing.
|
|
Slot string `json:"slot"`
|
|
// Code is never interpreted — it is the shell's syntax, and only the shell reads it.
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
// The shells and slots a contribution may name (novox/hq ADR 0204). Closed, so a typo is a refusal
|
|
// at the check rather than code that silently lands in no placeholder.
|
|
var (
|
|
knownShells = []string{"zsh", "bash", "fish"}
|
|
knownSlots = []string{"first", "normal", "last"}
|
|
// sessionFiles are the two files of the graphical session's start that read no directory, so a
|
|
// contribution to them is a slot rather than a drop-in (novox/hq ADR 0208 §4): `xinitrc` is POSIX
|
|
// code the session's start runs, `xresources` X resources merged at its start. Placed by the
|
|
// display server's holder, as a shell's slots are placed by the login shell's.
|
|
sessionFiles = []string{"xinitrc", "xresources"}
|
|
// powerMoments are the moments of a machine's power a module may run code at (novox/hq ADR
|
|
// 0211 §3): POSIX code run as root by node-power's holder, in module order, each piece bounded.
|
|
powerMoments = []string{"after-boot", "before-sleep", "after-wake", "before-shutdown", "on-mains", "on-battery"}
|
|
)
|
|
|
|
// contributionTargets is every name a contribution's `for` may take.
|
|
func contributionTargets() []string {
|
|
out := append(append([]string(nil), knownShells...), sessionFiles...)
|
|
return append(out, powerMoments...)
|
|
}
|
|
|
|
// placerOf is the seat whose holder places a contribution for this target (novox/hq ADR 0204,
|
|
// ADR 0208 §4).
|
|
func placerOf(target string) string {
|
|
if oneOf(sessionFiles, target) {
|
|
return DisplayServerSeat
|
|
}
|
|
if oneOf(powerMoments, target) {
|
|
return PowerSeat
|
|
}
|
|
return LoginShellSeat
|
|
}
|
|
|
|
// The two renderings of the environment a holder may place (novox/hq ADR 0203, decision 3).
|
|
const (
|
|
EnvironmentPOSIX = "posix"
|
|
EnvironmentSystemd = "systemd"
|
|
)
|
|
|
|
// ofEnvironment and ofShell are where a holder places what was contributed: ${environment:posix},
|
|
// ${environment:systemd} and ${shell:<shell>:<slot>}. Loose inside the braces on purpose, so a
|
|
// misspelt key is found and refused rather than left in a file as a literal nobody reads.
|
|
var (
|
|
ofEnvironment = regexp.MustCompile(`\$\{environment:([^}]*)\}`)
|
|
ofShell = regexp.MustCompile(`\$\{shell:([^}]*)\}`)
|
|
)
|
|
|
|
// variableName is a POSIX shell variable name, which is also what environment.d accepts.
|
|
var variableName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
|
|
|
// environmentProblems is what is wrong with this module's environment contribution, from the
|
|
// manifest alone.
|
|
func (m Manifest) environmentProblems() []string {
|
|
if m.Environment == nil {
|
|
return nil
|
|
}
|
|
var problems []string
|
|
for _, n := range sortedKeys(m.Environment.Variables) {
|
|
switch {
|
|
case !variableName.MatchString(n):
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s sets the variable %q, which is not a name a shell accepts: a letter or an "+
|
|
"underscore, then letters, digits and underscores", m.Module, n))
|
|
continue
|
|
case n == "PATH":
|
|
// PATH is the one variable every module shares, so no module may set it whole: a second
|
|
// setter would replace the first's entries, and the account's own PATH with them.
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s sets PATH as a variable; a module adds an entry under environment.path, at the "+
|
|
"start or the end, and PATH is composed from every module's (novox/hq ADR 0203)", m.Module))
|
|
continue
|
|
}
|
|
if why := literalProblem(m.Environment.Variables[n]); why != "" {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s sets %s to %q, which %s — %s", m.Module, n, m.Environment.Variables[n], why, literalRule))
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
for i, p := range m.Environment.Path {
|
|
switch {
|
|
case p.Entry == "":
|
|
problems = append(problems, fmt.Sprintf("%s's PATH entry %d names no directory", m.Module, i+1))
|
|
case strings.Contains(ofMachine.ReplaceAllString(p.Entry, ""), ":"):
|
|
// A colon is PATH's own separator, so an entry holding one is two entries, and the
|
|
// check that it is already present would look for the wrong thing.
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s puts %q on PATH, which holds a colon, PATH's own separator", m.Module, p.Entry))
|
|
case seen[p.Entry]:
|
|
problems = append(problems, fmt.Sprintf("%s puts %q on PATH twice", m.Module, p.Entry))
|
|
default:
|
|
if why := literalProblem(p.Entry); why != "" {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s puts %q on PATH, which %s — %s", m.Module, p.Entry, why, literalRule))
|
|
}
|
|
}
|
|
seen[p.Entry] = true
|
|
if p.At != PathAtStart && p.At != PathAtEnd {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s puts %q on PATH at %q; an entry goes at %q or %q of the account's PATH",
|
|
m.Module, p.Entry, p.At, PathAtStart, PathAtEnd))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// literalRule is why a value must be literal, said with every refusal of one.
|
|
const literalRule = "a value is literal, so a POSIX shell and the service manager read it alike, and " +
|
|
"names the machine only through the mesh's own ${machine:…} facts (novox/hq ADR 0203)"
|
|
|
|
// literalProblem is why a value cannot be written, unquoted by either reader, as the same string in
|
|
// both formats — or nothing. A `$` would expand differently in each; a quote or a backslash is
|
|
// quoting in one and a character in the other; a line break ends the line in both.
|
|
func literalProblem(v string) string {
|
|
switch {
|
|
case strings.ContainsAny(v, `'"`):
|
|
return "holds a quote"
|
|
case strings.Contains(v, `\`):
|
|
return "holds a backslash"
|
|
case strings.ContainsAny(v, "\n\r"):
|
|
return "holds a line break"
|
|
case strings.ContainsRune(v, 0):
|
|
return "holds a NUL"
|
|
case strings.Contains(ofMachine.ReplaceAllString(v, ""), "$"):
|
|
return "holds a $ that is not one of the machine's ${machine:…} facts"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// shellProblems is what is wrong with this module's shell code, from the manifest alone. The code
|
|
// itself is not judged: it is the shell's syntax, which the controller does not read.
|
|
func (m Manifest) shellProblems() []string {
|
|
var problems []string
|
|
for i, c := range m.Shell {
|
|
if !oneOf(contributionTargets(), c.For) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's shell code %d is for %q; the shells are %s, the session's files %s, and the power "+
|
|
"moments %s", m.Module, i+1, c.For, strings.Join(knownShells, ", "),
|
|
strings.Join(sessionFiles, ", "), strings.Join(powerMoments, ", ")))
|
|
}
|
|
if !oneOf(knownSlots, c.Slot) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's shell code %d goes in the slot %q; the slots are %s", m.Module, i+1, c.Slot,
|
|
strings.Join(knownSlots, ", ")))
|
|
}
|
|
if strings.TrimSpace(c.Code) == "" {
|
|
problems = append(problems, fmt.Sprintf("%s's shell code %d has no code", m.Module, i+1))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// contributionPlaceholderProblems is every place this module's resources name the environment or
|
|
// the shell's code and may not — judged from the manifest, so the catalogue check refuses it before
|
|
// a mesh does, and again at composition in the same words.
|
|
func (m Manifest) contributionPlaceholderProblems() []string {
|
|
var problems []string
|
|
for _, r := range m.Resources {
|
|
problems = append(problems, placeholderProblems(m, r)...)
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// placeholderProblems is what is wrong with one resource's ${environment:…} and ${shell:…}.
|
|
//
|
|
// **The seat authorises it, not the placeholder** (novox/hq ADR 0203 §5, ADR 0204 §3), as the seat
|
|
// authorises the bus's user list: a module that does not hold the account's environment writing it
|
|
// would be a second writer of a file there is one of, and a module that does not hold the login
|
|
// shell writing every module's shell code would be a second shell.
|
|
func placeholderProblems(m Manifest, r map[string]any) []string {
|
|
var problems []string
|
|
for _, field := range sortedKeys(r) {
|
|
s, ok := r[field].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
env := ofEnvironment.FindAllStringSubmatch(s, -1)
|
|
code := ofShell.FindAllStringSubmatch(s, -1)
|
|
if len(env)+len(code) == 0 {
|
|
continue
|
|
}
|
|
if field != "content" {
|
|
// Placed only where a file's bytes are, which is where every one of them is meant to go:
|
|
// a path or an owner holding several lines of shell is nothing the host could act on.
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s in its %s; the environment and the shell's code are placed "+
|
|
"only in a file's content", m.Module, r["id"], placeholderOf(env, code), field))
|
|
continue
|
|
}
|
|
for _, e := range env {
|
|
if e[1] != EnvironmentPOSIX && e[1] != EnvironmentSystemd {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s; the environment is ${environment:%s} or ${environment:%s}",
|
|
m.Module, r["id"], e[0], EnvironmentPOSIX, EnvironmentSystemd))
|
|
}
|
|
}
|
|
if len(env) > 0 && !m.ClaimsSeat(EnvironmentSeat) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s and %s does not claim %s; the account's environment is "+
|
|
"written by that seat's holder alone (novox/hq ADR 0203)",
|
|
m.Module, r["id"], env[0][0], m.Module, EnvironmentSeat))
|
|
}
|
|
// Each placeholder judged by its own target: a shell's code is the login shell's holder's to
|
|
// place (ADR 0204), the session's files the display server's (ADR 0208 §4) — and a holder of
|
|
// one placing the other's would be a second writer of a file there is one of.
|
|
refusedFor := map[string]bool{}
|
|
for _, c := range code {
|
|
target, slot, two := strings.Cut(c[1], ":")
|
|
if !two || !oneOf(contributionTargets(), target) || !oneOf(knownSlots, slot) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s; shell code is ${shell:<shell>:<slot>}, the shell one of "+
|
|
"%s, the session's file one of %s or the power moment one of %s, and the slot one of %s",
|
|
m.Module, r["id"], c[0], strings.Join(knownShells, ", "), strings.Join(sessionFiles, ", "),
|
|
strings.Join(powerMoments, ", "), strings.Join(knownSlots, ", ")))
|
|
continue
|
|
}
|
|
seat := placerOf(target)
|
|
if m.ClaimsSeat(seat) || refusedFor[seat] {
|
|
continue
|
|
}
|
|
refusedFor[seat] = true
|
|
if seat == PowerSeat {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s and %s does not claim %s; every module's code for a power "+
|
|
"moment is placed by the power seat's holder alone (novox/hq ADR 0211)",
|
|
m.Module, r["id"], c[0], m.Module, seat))
|
|
continue
|
|
}
|
|
if seat == DisplayServerSeat {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s and %s does not claim %s; every module's %s is placed by "+
|
|
"the display server's holder alone (novox/hq ADR 0208)",
|
|
m.Module, r["id"], c[0], m.Module, seat, target))
|
|
continue
|
|
}
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s and %s does not claim %s; every module's shell code is "+
|
|
"placed by the login shell's holder alone (novox/hq ADR 0204)",
|
|
m.Module, r["id"], c[0], m.Module, seat))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
func placeholderOf(env, code [][]string) string {
|
|
if len(env) > 0 {
|
|
return env[0][0]
|
|
}
|
|
return code[0][0]
|
|
}
|
|
|
|
// contributedEnvironment is one node's environment, gathered and in the order it is written.
|
|
type contributedEnvironment struct {
|
|
// variables is by module in name order, each module's sorted by name.
|
|
variables []setBy
|
|
// start and end are PATH's entries in their final order, each once.
|
|
start, end []placedOn
|
|
}
|
|
|
|
type setBy struct {
|
|
module string
|
|
names []string
|
|
values map[string]string
|
|
}
|
|
|
|
type placedOn struct {
|
|
module, entry string
|
|
}
|
|
|
|
// inModuleOrder is the modules sorted by name — the order contributions are written in (novox/hq
|
|
// ADR 0203, ADR 0204), so the same set composes byte for byte whatever order they were assigned in.
|
|
func inModuleOrder(modules []Manifest) []Manifest {
|
|
out := append([]Manifest(nil), modules...)
|
|
sort.SliceStable(out, func(a, b int) bool { return out[a].Module < out[b].Module })
|
|
return out
|
|
}
|
|
|
|
// variablesSetOnce refuses a variable two modules on one node both set (novox/hq ADR 0203 §5),
|
|
// naming both. Neither is chosen: whichever was written last would win in one reader and not
|
|
// necessarily in the other, and the module that lost would not be told.
|
|
func variablesSetOnce(modules []Manifest) error {
|
|
setter := map[string]string{}
|
|
for _, m := range inModuleOrder(modules) {
|
|
if m.Environment == nil {
|
|
continue
|
|
}
|
|
for _, n := range sortedKeys(m.Environment.Variables) {
|
|
if first, taken := setter[n]; taken {
|
|
return fmt.Errorf(
|
|
"%s and %s both set %s on this machine; the account has one environment, so one "+
|
|
"of them must stop setting it (novox/hq ADR 0203)", first, m.Module, n)
|
|
}
|
|
setter[n] = m.Module
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// environmentOn gathers every module's environment on a node, with the machine's facts in place.
|
|
//
|
|
// A PATH entry two modules both add is written once, where the first puts it: two toolchains
|
|
// sharing ~/.local/bin is ordinary, and nothing about it is in conflict.
|
|
func environmentOn(modules []Manifest, facts map[string]string) (contributedEnvironment, error) {
|
|
var env contributedEnvironment
|
|
if err := variablesSetOnce(modules); err != nil {
|
|
return env, err
|
|
}
|
|
placed := map[string]bool{}
|
|
for _, m := range inModuleOrder(modules) {
|
|
if m.Environment == nil {
|
|
continue
|
|
}
|
|
if len(m.Environment.Variables) > 0 {
|
|
set := setBy{module: m.Module, values: map[string]string{}}
|
|
for _, n := range sortedKeys(m.Environment.Variables) {
|
|
v, err := factsIn(m.Environment.Variables[n], facts, m.Module, n)
|
|
if err != nil {
|
|
return env, err
|
|
}
|
|
set.names = append(set.names, n)
|
|
set.values[n] = v
|
|
}
|
|
env.variables = append(env.variables, set)
|
|
}
|
|
for _, p := range m.Environment.Path {
|
|
entry, err := factsIn(p.Entry, facts, m.Module, "a PATH entry")
|
|
if err != nil {
|
|
return env, err
|
|
}
|
|
if strings.Contains(entry, ":") {
|
|
return env, fmt.Errorf("%s puts %q on PATH on this machine, which holds a colon, PATH's own separator",
|
|
m.Module, entry)
|
|
}
|
|
if placed[entry] {
|
|
continue
|
|
}
|
|
placed[entry] = true
|
|
if p.At == PathAtEnd {
|
|
env.end = append(env.end, placedOn{m.Module, entry})
|
|
} else {
|
|
env.start = append(env.start, placedOn{m.Module, entry})
|
|
}
|
|
}
|
|
}
|
|
return env, nil
|
|
}
|
|
|
|
// factsIn resolves a contributed value's ${machine:…} facts with this machine's — first, before
|
|
// either format is written, so both say the same thing (novox/hq ADR 0203).
|
|
func factsIn(v string, facts map[string]string, module, what string) (string, error) {
|
|
for _, key := range machineUsed(v) {
|
|
value, has := facts[key]
|
|
if !has {
|
|
return "", fmt.Errorf("%s sets %s to a value that says ${machine:%s}, and this machine says %s",
|
|
module, what, key, orNothing(namesOfFacts(facts)))
|
|
}
|
|
v = strings.ReplaceAll(v, fmt.Sprintf("${machine:%s}", key), value)
|
|
}
|
|
// Judged again once filled: a fact is the mesh's, and still has to be a literal both readers
|
|
// take alike.
|
|
if why := literalProblem(v); why != "" {
|
|
return "", fmt.Errorf("%s sets %s to %q on this machine, which %s — %s", module, what, v, why, literalRule)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// posix is the environment as lines a POSIX shell sources (novox/hq ADR 0203 §3): every variable
|
|
// exported, every PATH entry added only when it is missing, so sourcing the file twice — a login
|
|
// shell that starts another — changes nothing. POSIX sh only, because sh, bash and zsh all read it.
|
|
//
|
|
// The start entries are written last-first: each is put in front of PATH, so the last written ends
|
|
// up first, and the result reads in module order, then the order each module declared.
|
|
func (e contributedEnvironment) posix() string {
|
|
var b strings.Builder
|
|
for _, set := range e.variables {
|
|
fmt.Fprintf(&b, "# %s\n", set.module)
|
|
for _, n := range set.names {
|
|
fmt.Fprintf(&b, "export %s='%s'\n", n, set.values[n])
|
|
}
|
|
}
|
|
named := ""
|
|
for i := len(e.start) - 1; i >= 0; i-- {
|
|
p := e.start[i]
|
|
if p.module != named {
|
|
fmt.Fprintf(&b, "# %s\n", p.module)
|
|
named = p.module
|
|
}
|
|
fmt.Fprintf(&b, "case \":${PATH}:\" in *':%s:'*) ;; *) PATH='%s'\"${PATH:+:${PATH}}\" ;; esac\n",
|
|
p.entry, p.entry)
|
|
}
|
|
named = ""
|
|
for _, p := range e.end {
|
|
if p.module != named {
|
|
fmt.Fprintf(&b, "# %s\n", p.module)
|
|
named = p.module
|
|
}
|
|
fmt.Fprintf(&b, "case \":${PATH}:\" in *':%s:'*) ;; *) PATH=\"${PATH:+${PATH}:}\"'%s' ;; esac\n",
|
|
p.entry, p.entry)
|
|
}
|
|
if len(e.start)+len(e.end) > 0 {
|
|
b.WriteString("export PATH\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// systemd is the same environment as the service manager's environment.d reads it (novox/hq ADR
|
|
// 0203 §3), for the account's user manager and so for everything a graphical session starts. Read
|
|
// once per manager start, so it needs no guard against running twice; the account's existing PATH
|
|
// sits between the start and the end entries.
|
|
func (e contributedEnvironment) systemd() string {
|
|
var b strings.Builder
|
|
for _, set := range e.variables {
|
|
fmt.Fprintf(&b, "# %s\n", set.module)
|
|
for _, n := range set.names {
|
|
fmt.Fprintf(&b, "%s=%s\n", n, set.values[n])
|
|
}
|
|
}
|
|
if len(e.start) > 0 {
|
|
fmt.Fprintf(&b, "# %s\nPATH=%s${PATH:+:$PATH}\n", modulesOf(e.start), entriesOf(e.start))
|
|
}
|
|
if len(e.end) > 0 {
|
|
fmt.Fprintf(&b, "# %s\nPATH=${PATH:+$PATH:}%s\n", modulesOf(e.end), entriesOf(e.end))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// modulesOf names who contributed a line holding several modules' entries, in the order they appear.
|
|
func modulesOf(entries []placedOn) string {
|
|
var names []string
|
|
seen := map[string]bool{}
|
|
for _, p := range entries {
|
|
if !seen[p.module] {
|
|
seen[p.module] = true
|
|
names = append(names, p.module)
|
|
}
|
|
}
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
func entriesOf(entries []placedOn) string {
|
|
out := make([]string, len(entries))
|
|
for i, p := range entries {
|
|
out[i] = p.entry
|
|
}
|
|
return strings.Join(out, ":")
|
|
}
|
|
|
|
// shellCode is every module's code for one shell and one slot (novox/hq ADR 0204 §3): in module
|
|
// order, each module's pieces in the order it declared them, each preceded by a line naming the
|
|
// module, and empty when nothing is contributed.
|
|
func shellCode(modules []Manifest, shell, slot string) string {
|
|
var b strings.Builder
|
|
for _, m := range inModuleOrder(modules) {
|
|
named := false
|
|
for _, c := range m.Shell {
|
|
if c.For != shell || c.Slot != slot {
|
|
continue
|
|
}
|
|
if !named {
|
|
fmt.Fprintf(&b, "# %s\n", m.Module)
|
|
named = true
|
|
}
|
|
b.WriteString(c.Code)
|
|
if !strings.HasSuffix(c.Code, "\n") {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// contributionsInto fills a holder's file with the node's environment and its shell code.
|
|
//
|
|
// **Last, after every other placeholder pass, and in one pass each.** Shell code is contributed text
|
|
// in a shell's own syntax — `${XDG_CACHE_HOME:-$HOME/.cache}`, `${(%):-%n}` — and the rendered
|
|
// environment holds `${PATH:+…}`: a scanner for the mesh's own placeholders that ran after these
|
|
// were in place would read the shell's expansions as the mesh's and refuse them, or fill a
|
|
// `${machine:…}` some module wrote for its shell to see. So nothing runs after them, the environment
|
|
// is filled before the shell's code is, and each is replaced in a single pass over what the holder
|
|
// wrote, so a contributed piece is never scanned again.
|
|
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string) error {
|
|
if problems := placeholderProblems(m, resource); len(problems) > 0 {
|
|
return fmt.Errorf("%s", problems[0])
|
|
}
|
|
content, ok := resource["content"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if ofEnvironment.MatchString(content) {
|
|
env, err := environmentOn(modules, facts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content = ofEnvironment.ReplaceAllStringFunc(content, func(placeholder string) string {
|
|
if ofEnvironment.FindStringSubmatch(placeholder)[1] == EnvironmentSystemd {
|
|
return env.systemd()
|
|
}
|
|
return env.posix()
|
|
})
|
|
}
|
|
if ofShell.MatchString(content) {
|
|
content = ofShell.ReplaceAllStringFunc(content, func(placeholder string) string {
|
|
shell, slot, _ := strings.Cut(ofShell.FindStringSubmatch(placeholder)[1], ":")
|
|
return shellCode(modules, shell, slot)
|
|
})
|
|
}
|
|
resource["content"] = content
|
|
return nil
|
|
}
|