summaryrefslogtreecommitdiff
path: root/internal/config/load.go
blob: cd4d97cc212688cef4821e6927a83c903746b027 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// SPDX-License-Identifier: GPL-3.0-or-later

package config

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"krino/internal/sexp"
	"krino/internal/xdg"
)

// Config is the whole configuration: the main file and the directories it
// includes.
type Config struct {
	Main *Main
	Dirs []*Dir
}

// DefaultFile is krino.conf in the XDG config directory.
func DefaultFile() string {
	return filepath.Join(xdg.ConfigHome(), "krino", "krino.conf")
}

// DirFile is where directory name's config lives, beside the main file.
func DirFile(mainFile, name string) string {
	return filepath.Join(filepath.Dir(mainFile), "dirs", name+".conf")
}

// Load reads the main file and the files of the directories it includes.
// With names, only those directories are read, and each must be included.
// The Config is nil only when the main file itself cannot be read.
func Load(mainFile string, names ...string) (*Config, []*Diag) {
	src, err := os.ReadFile(mainFile)
	if errors.Is(err, fs.ErrNotExist) {
		return nil, []*Diag{{File: mainFile, Msg: "not found; create it with: krino init"}}
	}
	if err != nil {
		return nil, []*Diag{{File: mainFile, Msg: err.Error()}}
	}
	m, errs := ParseMain(mainFile, src)
	cfg := &Config{Main: m}
	if _, perr := sexp.Parse(mainFile, src); perr != nil {
		// krino.conf itself is unreadable: report just that, and load none
		// of the directories, so a syntax error never also produces
		// "NAME is not in include" for names the caller asked for.
		return cfg, errs
	}
	want := m.Include
	if len(names) > 0 {
		want = nil
		for _, n := range names {
			if _, ok := m.IncludePos[n]; !ok {
				errs = append(errs, &Diag{File: mainFile, Msg: fmt.Sprintf("%q is not in include", n)})
				continue
			}
			want = append(want, n)
		}
	}
	for _, name := range want {
		file := DirFile(mainFile, name)
		src, err := os.ReadFile(file)
		if err != nil {
			msg := err.Error()
			if errors.Is(err, fs.ErrNotExist) {
				msg = fmt.Sprintf("included %q, but %s does not exist; create it with: krino new %s PATH", name, file, name)
			}
			errs = append(errs, &Diag{File: mainFile, Pos: m.IncludePos[name], Msg: msg})
			continue
		}
		dir, derrs := ParseDir(name, file, src)
		errs = append(errs, derrs...)
		cfg.Dirs = append(cfg.Dirs, dir)
	}
	return cfg, errs
}

// Resolved is the settings that apply in dir: built-in, then the main
// file's defaults, then the directory's own.
func (c *Config) Resolved(dir *Dir) Resolved {
	return dir.Settings.Over(c.Main.Defaults.Over(Builtin()))
}

// LogFile is the main file's (log ...), or the default under XDG_STATE_HOME.
func (c *Config) LogFile() string {
	if c.Main.Log != "" {
		return c.Main.Log
	}
	return filepath.Join(xdg.StateHome(), "krino", "krino.log")
}

// LockFile is where a directory's lock lives while a run is active:
// $XDG_STATE_HOME/krino/<name>.lock, beside the log (spec §3).
func (c *Config) LockFile(name string) string {
	return filepath.Join(xdg.StateHome(), "krino", name+".lock")
}