domain/architecture/system/deployment/nix_darwin/brew/effects.bend

near-v-framework@0.5.0.0 · 95 lines · 0 definitions

1import Base
2import near-v-framework@0.5.0.0/domain/architecture/system/effs/io.bend as X
3import near-v-framework@0.5.0.0/domain/architecture/system/plan/type.bend as P
4import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/brew/type.bend as Br
5import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/brew/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
10def declared_file() -> String:
11  "/tmp/v-declared.Brewfile"
12
13def 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.
18def bundle(args: List<String>) -> 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 ----
22def 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
29def 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
40def 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
44def 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 ----
52def 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.
57def 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
63def 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
75def 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.
83def changes(brews: List<&2, Br.Brew>) -> IO(List<&2, P.Step>):
84  do IO<List<&2, P.Step>>:
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.
91def observed() -> IO(List<&2, Br.Brew>):
92  do IO<List<&2, Br.Brew>>:
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), [])