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))