1 import Base 2 import ../lib/effs/io.bend as X 3 import ../lib/vstr.bend as S 4 import ./project.bend as P 5 import ./vimport.bend as I 6 import ./vmac.bend as V 7 8 # `v system dryrun`: what applying the declared Mac (bend/mac.bend) would change, without changing anything. 9 10 def 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 ') -> Bool: 16 Bool.not(String.starts_with(S.nth(parts, 1n), ") -> 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 21 def 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 24 def 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. 28 def 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 35 def 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 42 def drop_zeros(s: String) -> String: 43 String.from_list(dz(String.to_list(s))) 44 45 def trim_dot(+s: String) -> String: 46 I.pick(String.ends_with(s, "."), String.take(s, Nat.sub(String.length(s), 1n)), s) 47 48 49 def trim_num(+s: String) -> String: 50 I.pick(String.contains(s, "."), trim_dot(String.reverse(drop_zeros(String.reverse(s)))), s) 51 52 def same(+now: String, +want: String) -> Bool: 53 String.eq(now, want) || String.eq(trim_num(now), trim_num(want)) 54 55 def unset_or(+v: String) -> String: 56 I.pick(String.is_empty(v), "(unset)", v) 57 58 def 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: 64 IO.print("change " ++ dom ++ " " ++ key ++ ": " ++ now ++ " -> " ++ want) 65 return 1n 66 67 68 def compare(+host: String, +dom: String, +key: String, +want: String) -> IO(Nat): 69 do IO: 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 74 def 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. 78 def stop_at(stop: Bool, xs: List<&2, String>) -> List<&2, String>: 79 match stop: 80 case True{}: 81 [] 82 case False{}: 83 xs 84 85 def xml_string(+l: String) -> String: 86 S.nth(String.split(S.nth(String.split(l, '>'), 1n), '<'), 0n) 87 88 def 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, ""), S.append(S.line_if(prev, V.url_path(xml_string(l))), written_paths(rest, String.contains(l, "_CFURLString")))) 94 95 def 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 102 def check_array(+dom: String, +key: String, rest: List<&2, String>) -> IO(Nat): 103 do IO: 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 109 def 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 116 def check_write(+l: String, +rest: List<&2, String>) -> IO(Nat): 117 check_kind(String.starts_with(String.trim(S.nth(rest, 2n)), ""), l, rest) 118 119 def one_if(b: Bool) -> Nat: 120 I.one_if(b) 121 122 123 def 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 129 def 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 136 def 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: 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 ---- 148 def pick_idx(q: Bool) -> Nat: 149 match q: 150 case True{}: 151 1n 152 case False{}: 153 0n 154 155 156 def 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 160 def 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 167 def 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 170 def 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 177 def brew_check(+bf: String) -> IO(Unit): 178 do IO: 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 184 def 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 191 def 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 196 def dryrun() -> IO(Unit): 197 do IO: 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))