-
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
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
package main
import (
"errors"
"flag"
"fmt"
"io/fs"
"log"
"os"
"os/exec"
"strings"
)
// Path to the home-manager bin.
// Normally this will be overridden at compile time via "ldflags -X main.homeManagerPath=<path>".
var homeManagerPath = "home-manager"
func main() {
help := flag.Bool("help", false, "Print usage text to stdout.")
verbose := flag.Bool("verbose", false, "Enable verbose logging.")
flag.Parse()
logger := log.New(os.Stderr, "", 0)
if *help {
fmt.Print(helpText())
os.Exit(0)
}
args := flag.Args()
if len(args) < 1 {
fmt.Fprint(os.Stderr, helpText())
logger.Fatal("No command provided.")
}
switch args[0] {
case "set":
if len(args) != 2 {
fmt.Fprint(os.Stderr, helpText())
logger.Fatal("`set` requires exact 1 argument")
}
setSpecialisation(args[1], *verbose, logger)
case "unset":
activateNonSpecialisedProfile(logger)
case "clean":
clean(logger)
default:
fmt.Fprint(os.Stderr, helpText())
logger.Fatalf("Unknown command: %s", args[0])
}
}
func helpText() string {
binName := os.Args[0]
return fmt.Sprintf(`%s - Manage Home-Manager specialisation easily.
[Usage]
%s set <specialisation>
%s unset
[COMMANDS]
set <specialisation>
Activate the particular specialisation.
unset
Activate the most recent home-manager profile which have specialisations.
clean
Delete generations other than the latest generation and the latest generation having
specialisation directory.
[OPTIONS]
--help
Print this message to stdout.
--verbose
Enable verbose logging.
`, binName, binName, binName)
}
func setSpecialisation(specialisation string, verbose bool, logger *log.Logger) {
if strings.Contains(specialisation, "/") {
logger.Fatalf("specialisation cannot have slash (\"/\"): %s", specialisation)
}
generations, err := Generations(logger)
if err != nil {
logger.Fatalf("Failed to get generations: %s", err)
}
for _, generation := range generations {
specialisations, err := generation.Specialisations()
if err != nil {
logger.Fatalf("Failed to get specialisations for %s: %s", generation.ID, err)
}
if verbose {
logger.Printf("Found generation with ID=%s", generation.ID)
}
for _, s := range specialisations {
if verbose {
logger.Printf("Found specialisation with Name=%s in generation ID=%s", s.Name, generation.ID)
}
if s.Name != specialisation {
continue
}
logger.Printf("Activate %s from ID=%s", specialisation, generation.ID)
if err := s.Profile.Activate(); err != nil {
logger.Fatalf("Failed to activate %s: %s", specialisation, err)
}
logger.Printf("Activated %s", specialisation)
os.Exit(0)
}
}
logger.Fatalf("No specialisation named \"%s\" found", specialisation)
}
func activateNonSpecialisedProfile(logger *log.Logger) {
generations, err := Generations(logger)
if err != nil {
logger.Fatalf("Failed to get generations: %s", err)
}
for _, generation := range generations {
specialisations, err := generation.Specialisations()
if err != nil {
logger.Fatalf("Failed to check specialisations (ID=%s): %s", generation.ID, err)
}
if len(specialisations) == 0 {
continue
}
logger.Printf("Activate generation (ID=%s)", generation.ID)
if err := generation.Profile.Activate(); err != nil {
logger.Fatalf("Failed to activate: %s", err)
}
os.Exit(0)
}
logger.Fatal("No generation having specialisations found: Manually switch to a profile using home-manager.")
}
func clean(logger *log.Logger) {
generations, err := Generations(logger)
if err != nil {
logger.Fatalf("Failed to get generations: %s", err)
}
foundGenerationHavingSpecialisation := false
args := []string{"remove-generations"}
for i, generation := range generations {
if !foundGenerationHavingSpecialisation {
specialisations, err := generation.Specialisations()
if err != nil {
logger.Fatalf("Failed to get specialisations for ID=%s: %s", generation.ID, err)
}
if len(specialisations) > 0 {
foundGenerationHavingSpecialisation = true
continue
}
}
if i == 0 {
continue
}
logger.Printf("Removing generation ID=%s", generation.ID)
args = append(args, generation.ID)
}
if len(args) == 1 {
logger.Print("Nothing to clean.")
os.Exit(0)
}
cmd := exec.Command(homeManagerPath, args...)
var out strings.Builder
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
logger.Fatalf("Failed to remove generations: %s", err)
}
}
type Profile struct {
Path string
}
func (p Profile) Activate() error {
dir := os.DirFS(p.Path)
bin, err := dir.Open("activate")
if err != nil {
return err
}
defer bin.Close()
stat, err := bin.Stat()
if err != nil {
return err
}
if stat.IsDir() {
return fmt.Errorf("Cannot activate %s: target is directory", p.Path)
}
path := fmt.Sprintf("%s/activate", p.Path)
cmd := exec.Command(path)
return cmd.Run()
}
type Specialisation struct {
Name string
Profile Profile
}
type Generation struct {
ID string
Profile Profile
}
func ParseGeneration(line string) (*Generation, error) {
tokens := strings.Split(line, " ")
colonPosition := -1
for i, token := range tokens {
if token == ":" {
colonPosition = i
break
}
}
if colonPosition < 0 {
return nil, errors.New("Unexpected generation output line: No colon found")
}
if len(tokens) < colonPosition+4 {
return nil, errors.New("Unexpected generation output line: Missing tokens")
}
if tokens[colonPosition+1] != "id" {
return nil, fmt.Errorf("Unexpected generation output line: Expected `id`, found `%s`", tokens[colonPosition+1])
}
id := tokens[colonPosition+2]
if tokens[colonPosition+3] != "->" {
return nil, fmt.Errorf("Unexpected generation output line: Expected `->`, found `%s`", tokens[colonPosition+3])
}
path := strings.Join(tokens[(colonPosition+4):], " ")
return &Generation{
ID: id,
Profile: Profile{
Path: path,
},
}, nil
}
func Generations(logger *log.Logger) ([]Generation, error) {
cmd := exec.Command(homeManagerPath, "generations")
var out strings.Builder
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, err
}
stdout := out.String()
lines := strings.Split(stdout, "\n")
generations := make([]Generation, 0, len(lines))
for _, line := range strings.Split(stdout, "\n") {
if len(strings.TrimSpace(line)) == 0 {
continue
}
tokens := strings.Split(line, " ")
if len(tokens) == 0 {
continue
}
generation, err := ParseGeneration(line)
if err != nil {
logger.Printf("Failed to parse generation output: %s", err.Error())
continue
}
generations = append(generations, *generation)
}
return generations, nil
}
func (g Generation) Specialisations() ([]Specialisation, error) {
dir := os.DirFS(g.Profile.Path)
entries, err := fs.ReadDir(dir, "specialisation")
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return []Specialisation{}, nil
}
return nil, err
}
specialisations := make([]Specialisation, 0, len(entries))
for _, entry := range entries {
name := entry.Name()
specialisations = append(specialisations, Specialisation{
Name: name,
Profile: Profile{
Path: fmt.Sprintf("%s/specialisation/%s", g.Profile.Path, name),
},
})
}
return specialisations, nil
}