cli/vdry.bend
near-v-framework@0.2.0.0 · 212 lines · 0 definitions
1import Base
2import near-v-framework@0.2.0.0/lib/effs/io.bend as X
3import near-v-framework@0.2.0.0/lib/vstr.bend as S
4import near-v-framework@0.2.0.0/cli/project.bend as P
5import near-v-framework@0.2.0.0/cli/vimport.bend as I
6import near-v-framework@0.2.0.0/cli/vmac.bend as V
7
8# `v system dryrun`: what applying the declared Mac (bend/mac.bend) would change, without changing anything.
9
10def pick(c: Bool, a: String, b: String) -> String:
11 I.pick(c, a, b)
12
13# ---- preferences nix-darwin would write ----
14# The write line's domain and key: "... write DOMAIN KEY '<?xml" or "... write DOMAIN 'KEY WITH SPACES' '<?xml".
15def quoted_key(+parts: List<&2, String>) -> Bool:
16 Bool.not(String.starts_with(S.nth(parts, 1n), "<?xml"))
17
18def domain_of(+parts: List<&2, String>) -> String:
19 pick(quoted_key(parts), S.last(S.words(S.nth(parts, 0n))), S.nth(S.words(S.nth(parts, 0n)), Nat.sub(S.count(S.words(S.nth(parts, 0n))), 2n)))
20
21def key_of(+parts: List<&2, String>) -> String:
22 pick(quoted_key(parts), S.nth(parts, 1n), S.last(S.words(S.nth(parts, 0n))))
23
24def is_write(+l: String) -> Bool:
25 String.contains(l, "defaults write ") || String.contains(l, "defaults -currentHost write ")
26
27# "0.500000" -> "0.5", "0.000000" -> "0": numbers compare by value, not spelling.
28def pick_chars(c: Bool, a: List<&2, Char>, b: List<&2, Char>) -> List<&2, Char>:
29 match c:
30 case True{}:
31 a
32 case False{}:
33 b
34
35def dz(cs: List<&2, Char>) -> List<&2, Char>:
36 match cs:
37 case []:
38 []
39 case +c <> +rest:
40 pick_chars(Char.is_eq(c, '0'), dz(rest), c <> rest)
41
42def drop_zeros(s: String) -> String:
43 String.from_list(dz(String.to_list(s)))
44
45def trim_dot(+s: String) -> String:
46 I.pick(String.ends_with(s, "."), String.take(s, Nat.sub(String.length(s), 1n)), s)
47
48
49def trim_num(+s: String) -> String:
50 I.pick(String.contains(s, "."), trim_dot(String.reverse(drop_zeros(String.reverse(s)))), s)
51
52def same(+now: String, +want: String) -> Bool:
53 String.eq(now, want) || String.eq(trim_num(now), trim_num(want))
54
55def unset_or(+v: String) -> String:
56 I.pick(String.is_empty(v), "(unset)", v)
57
58def report(ok: Bool, dom: String, key: String, now: String, want: String) -> IO(Nat):
59 match ok:
60 case True{}:
61 IO.pure(Nat, 0n)
62 case False{}:
63 do IO<Nat>:
64 IO.print("change " ++ dom ++ " " ++ key ++ ": " ++ now ++ " -> " ++ want)
65 return 1n
66
67
68def compare(+host: String, +dom: String, +key: String, +want: String) -> IO(Nat):
69 do IO<Nat>:
70 out : String <- X.sh(S.export_key(host ++ "export " ++ dom, key))
71 +now : String = unset_or(S.plist_scalar(out))
72 report(same(now, want), dom, key, now, want)
73
74def check_scalar(+l: String, rest: List<&2, String>) -> IO(Nat):
75 compare(pick(String.contains(l, "-currentHost"), "-currentHost ", ""), domain_of(String.split(l, '\'')), key_of(String.split(l, '\'')), S.plist_scalar(String.trim(S.nth(rest, 2n))))
76
77# Arrays nix-darwin writes are Dock tiles: compare the paths they point at, in order.
78def stop_at(stop: Bool, xs: List<&2, String>) -> List<&2, String>:
79 match stop:
80 case True{}:
81 []
82 case False{}:
83 xs
84
85def xml_string(+l: String) -> String:
86 S.nth(String.split(S.nth(String.split(l, '>'), 1n), '<'), 0n)
87
88def written_paths(ls: List<&2, String>, prev: Bool) -> List<&2, String>:
89 match ls:
90 case []:
91 []
92 case +l <> rest:
93 stop_at(String.contains(l, "</plist>"), S.append(S.line_if(prev, V.url_path(xml_string(l))), written_paths(rest, String.contains(l, "_CFURLString</key>"))))
94
95def paths(ls: List<&2, String>) -> List<&2, String>:
96 match ls:
97 case []:
98 []
99 case l <> rest:
100 V.url_path(String.trim(l)) <> paths(rest)
101
102def check_array(+dom: String, +key: String, rest: List<&2, String>) -> IO(Nat):
103 do IO<Nat>:
104 now : String <- X.sh("defaults read " ++ dom ++ " '" ++ key ++ "' 2>/dev/null | grep '\"_CFURLString\" =' | sed -E 's/.*= \"?([^\";]*)\"?;.*/\\1/'")
105 +a : String = String.join(paths(V.lines_of(now)), ", ")
106 +b : String = String.join(written_paths(rest, False{}), ", ")
107 report(String.eq(a, b), dom, key, "[" ++ a ++ "]", "[" ++ b ++ "]")
108
109def check_kind(arr: Bool, +l: String, rest: List<&2, String>) -> IO(Nat):
110 match arr:
111 case True{}:
112 check_array(domain_of(String.split(l, '\'')), key_of(String.split(l, '\'')), rest)
113 case False{}:
114 check_scalar(l, rest)
115
116def check_write(+l: String, +rest: List<&2, String>) -> IO(Nat):
117 check_kind(String.starts_with(String.trim(S.nth(rest, 2n)), "<array>"), l, rest)
118
119def one_if(b: Bool) -> Nat:
120 I.one_if(b)
121
122
123def add_pair(n: Nat, d: Nat, r: Nat & Nat) -> Nat & Nat:
124 match r:
125 case (a, b):
126 (Nat.add(n, a), Nat.add(d, b))
127
128
129def write_if(yes: Bool, +l: String, +rest: List<&2, String>) -> IO(Nat):
130 match yes:
131 case True{}:
132 check_write(l, rest)
133 case False{}:
134 IO.pure(Nat, 0n)
135
136def writes(ls: List<&2, String>) -> IO(Nat & Nat):
137 match ls:
138 case []:
139 IO.pure(Nat & Nat, (0n, 0n))
140 case +l <> +rest:
141 do IO<Nat & Nat>:
142 d : Nat <- write_if(is_write(l), l, rest)
143 r : Nat & Nat <- writes(rest)
144 return add_pair(one_if(is_write(l)), d, r)
145
146
147# ---- Homebrew, checked by Homebrew against nix-darwin's own Brewfile ----
148def pick_idx(q: Bool) -> Nat:
149 match q:
150 case True{}:
151 1n
152 case False{}:
153 0n
154
155
156def brewfile_clean(+w: String) -> String:
157 S.nth(String.split(S.last(String.split(w, '=')), '\''), pick_idx(String.starts_with(S.last(String.split(w, '=')), "'")))
158
159
160def brewfile_in(ws: List<&2, String>) -> String:
161 match ws:
162 case []:
163 ""
164 case +w <> rest:
165 I.first_of(pick(String.contains(w, "-Brewfile"), brewfile_clean(w), ""), brewfile_in(rest))
166
167def interesting(+l: String) -> Bool:
168 Bool.not(String.is_empty(l)) && Bool.not(String.starts_with(l, "Would")) && Bool.not(String.contains(l, "/Caches/")) && Bool.not(String.contains(l, "/Cellar/")) && Bool.not(String.contains(l, "/vendor/")) && Bool.not(String.contains(l, "tmp/")) && Bool.not(String.contains(l, "empty directory")) && Bool.not(String.starts_with(l, "Run `"))
169
170def removals(ls: List<&2, String>) -> String:
171 match ls:
172 case []:
173 ""
174 case +l <> rest:
175 pick(interesting(String.trim(l)), "remove " ++ String.trim(l) ++ "\n", "") ++ removals(rest)
176
177def brew_check(+bf: String) -> IO(Unit):
178 do IO<Unit>:
179 ok : Bool <- X.ok("brew bundle check --no-upgrade --file " ++ bf ++ " >/dev/null 2>&1")
180 IO.print(pick(ok, "", "install (Homebrew: declared entries missing; brew bundle check --verbose --file " ++ bf ++ ")"))
181 cl : String <- X.sh("brew bundle cleanup --file " ++ bf ++ " 2>&1")
182 IO.print(removals(String.lines(cl)))
183
184def drop_marker(ls: List<&2, String>) -> String:
185 match ls:
186 case []:
187 ""
188 case +l <> rest:
189 pick(String.contains(l, "(end of dry run)") || String.is_empty(String.trim(l)), "", l ++ "\n") ++ drop_marker(rest)
190
191def summary(r: Nat & Nat) -> String:
192 match r:
193 case (n, d):
194 "dry run: " ++ Nat.show(n) ++ " preferences checked, " ++ Nat.show(d) ++ " would change"
195
196def dryrun() -> IO(Unit):
197 do IO<Unit>:
198 I.import_mac()
199 +f : P.Facts <- P.read_facts()
200 diff : String <- P.run_bend("mac.dryrun.bend")
201 IO.print(drop_marker(String.lines(diff)))
202 flake : String <- P.run_bend("flake.bend")
203 X.write_file("nix/flake.nix", flake)
204 mac : String <- P.run_bend("mac.gen.bend")
205 X.write_file("nix/mac.nix", mac)
206 staged : Bool <- X.ok("git add nix")
207 sys : String <- X.sh("/nix/var/nix/profiles/default/bin/nix build \"$(pwd)/nix#darwinConfigurations." ++ P.flake_host(f) ++ ".system\" --no-link --print-out-paths 2>/dev/null")
208 act : String <- X.sh("cat " ++ String.trim(sys) ++ "/activate")
209 +lines : List<&2, String> = String.lines(act)
210 brew_check(brewfile_in(S.words(String.join(lines, " "))))
211 r : Nat & Nat <- writes(lines)
212 IO.print(summary(r))