-
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
// Copyright 2025 Shota FUJI <pockawoooh@gmail.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
package main
import (
"errors"
"flag"
"fmt"
"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)
}
clean(logger, *verbose)
}
func helpText() string {
binName := os.Args[0]
return fmt.Sprintf(`%s - Clean obsolete Home-Manager generations.
[Usage]
%s
[OPTIONS]
--help
Print this message to stdout.
--verbose
Enable verbose logging.
`, binName, binName)
}
func clean(logger *log.Logger, verbose bool) {
generations, err := Generations(logger)
if err != nil {
logger.Fatalf("Failed to get generations: %s", err)
}
args := []string{"remove-generations"}
for i, generation := range generations {
if verbose {
logger.Printf("Found generation ID=%s", generation.ID)
}
if i == 0 {
if verbose {
logger.Printf("Skipping initial generation ID=%s", generation.ID)
}
continue
}
logger.Printf("Removing generation ID=%s", generation.ID)
args = append(args, generation.ID)
}
if len(args) == 1 {
logger.Print("No generations to remove.")
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 Generation struct {
ID string
}
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])
}
return &Generation{
ID: id,
}, 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
}