1 import Base 2 import ../../../effs/io.bend as X 3 import ../../../plan/type.bend as P 4 import ./type.bend as Br 5 import ./ops.bend as BrO 6 7 # Homebrew as it is, and what applying the declared entries would change. Only brew bundle's read-only 8 # commands run: dump writes its temporary Brewfile, check and cleanup (without --force) only report. 9 10 def declared_file() -> String: 11 "/tmp/v-declared.Brewfile" 12 13 def observed_file() -> String: 14 "/tmp/v-observed.Brewfile" 15 16 # brew bundle exits 1 when something is missing or would be removed, and says what: Base's Process.run 17 # keeps the output of a failing program. 18 def bundle(args: List) -> IO(Result<&1, &1, U32 & String, U32 & (String & String)>): 19 Process.run("brew", "bundle" <> args, "", 67108864, 600000) 20 21 # ---- installs: `brew bundle check --verbose` says "→ Formula x needs to be installed." per entry ---- 22 def text_before(cs: List<&2, Char>, +pat: String) -> String: 23 match cs: 24 case []: 25 "" 26 case +c <> +rest: 27 Bool.pick(String, String.starts_with(String.from_list(c <> rest), pat), "", String.from_list([c]) ++ text_before(rest, pat)) 28 29 def missing(ls: List<&2, String>) -> List<&2, P.Step>: 30 match ls: 31 case []: 32 [] 33 case l <> rest: 34 +t = String.trim(l) 35 List.append(&2, P.Step, 36 Bool.pick(List<&2, P.Step>, String.starts_with(t, "→ "), 37 [P.Change{"homebrew", "(absent)", text_before(String.to_list(String.drop(t, 2n)), " needs to be ")}], []), 38 missing(rest)) 39 40 def some_or(+xs: List<&2, P.Step>, +f: String) -> List<&2, P.Step>: 41 Bool.pick(List<&2, P.Step>, List.is_empty(&2, P.Step, xs), 42 [P.Change{"homebrew", "declared entries missing", "see brew bundle check --verbose --file " ++ f}], xs) 43 44 def installs(r: Result<&1, &1, U32 & String, U32 & (String & String)>, +f: String) -> List<&2, P.Step>: 45 match r: 46 case Done{(status, (_, err))}: 47 Bool.pick(List<&2, P.Step>, U32.is_eq(status, 0), [], some_or(missing(String.lines(err)), f)) 48 case Fail{_}: 49 [P.Blocked{"homebrew", "brew bundle check could not run"}] 50 51 # ---- removals: `brew bundle cleanup` lists them under "Would uninstall casks:", "Would untap:" and so on ---- 52 def label_of(+heading: String) -> String: 53 Bool.pick(String, String.eq(heading, "casks"), "Cask", 54 Bool.pick(String, String.eq(heading, "formulae"), "Formula", heading)) 55 56 # The label of the section a line starts, the current one for an entry, or "" outside any section. 57 def section(+l: String, +label: String) -> String: 58 Bool.pick(String, String.starts_with(l, "Would uninstall ") && String.ends_with(l, ":"), 59 label_of(String.take(String.drop(l, 16n), Nat.sub(String.length(l), 17n))), 60 Bool.pick(String, String.eq(l, "Would untap:"), "Tap", 61 Bool.pick(String, String.is_empty(l) || String.starts_with(l, "Would ") || String.starts_with(l, "Run ") || String.starts_with(l, "==>"), "", label))) 62 63 def removals(ls: List<&2, String>, +label: String) -> List<&2, P.Step>: 64 match ls: 65 case []: 66 [] 67 case l <> rest: 68 +t = String.trim(l) 69 +next = section(t, label) 70 List.append(&2, P.Step, 71 Bool.pick(List<&2, P.Step>, Bool.not(String.is_empty(label)) && String.eq(next, label) && Bool.not(String.is_empty(t)), 72 [P.Change{"homebrew", label ++ " " ++ t, "(absent)"}], []), 73 removals(rest, next)) 74 75 def removed(r: Result<&1, &1, U32 & String, U32 & (String & String)>) -> List<&2, P.Step>: 76 match r: 77 case Done{(_, (out, _))}: 78 removals(String.lines(out), "") 79 case Fail{_}: 80 [P.Blocked{"homebrew", "brew bundle cleanup could not run"}] 81 82 # What applying the declared entries would install and remove. 83 def changes(brews: List<&2, Br.Brew>) -> IO(List<&2, P.Step>): 84 do IO>: 85 X.write(declared_file(), BrO.brewfile(brews)) 86 c : Result<&1, &1, U32 & String, U32 & (String & String)> <- bundle(["check", "--no-upgrade", "--verbose", "--file", declared_file()]) 87 r : Result<&1, &1, U32 & String, U32 & (String & String)> <- bundle(["cleanup", "--file", declared_file()]) 88 return List.append(&2, P.Step, installs(c, declared_file()), removed(r)) 89 90 # The Mac's Homebrew entries, as `brew bundle dump` writes them. 91 def observed() -> IO(List<&2, Br.Brew>): 92 do IO>: 93 ok : Bool <- X.succeeds("brew", ["bundle", "dump", "--force", "--file=" ++ observed_file()]) 94 text : String <- X.read(observed_file()) 95 return Bool.pick(List<&2, Br.Brew>, ok, BrO.of_brewfile(text), [])