src/tool/world.bend

ezx@1.6.0.0 · 232 lines · 0 definitions

1# tool/world: what `ez tool run`, `install` and `upgrade` read. The act and
2# the target they were given; the directory they run in (`here`), which a
3# relative cache root is taken from; the variables that place the cache and
4# the link (`EZ_TOOL_BIN`, `XDG_BIN_HOME`, `XDG_CACHE_HOME`, `HOME`, `PATH`);
5# the bend on the PATH, as its banner's first line; the file `--entry` names,
6# "" when it names none; the project's ledger and lock, None when there is
7# none; and the answers to the questions the planner (tool/plan.bend)
8# asked. `ez tool sync` reads only the ledger and the lock.
9#
10# A question is a file's text, whether a binary is there, what git says of a
11# local path, or one of the upgrade's (lock/up.bend): `Head` for the commit a
12# remote's HEAD names, and `Clone` for the checkout at a rev with every
13# file's text. The upgrade's are answered by the lock's interpreter and kept
14# apart, in `ups`, so its lookups serve here as they are.
15#
16# Everything else here is a projection: what a World says, read one way.
17import Base
18import ezx@1.6.0.0/src/lock/up.bend as Up
19import ezx@1.6.0.0/src/ledger/manifest.bend as M
20import ezx@1.6.0.0/src/pkg/path.bend as P
21
22# what the command does with the binary once it is current: `Run` starts it
23# with these words, `Install` and `Upgrade` link it onto PATH
24type Act is Data:
25  Run{args: List<&2, String>}
26  Install{}
27  Upgrade{}
28
29# the variables the command reads, each "" when it is unset
30type Env is Data:
31  Env{tool_bin: String, xdg_bin: String, xdg_cache: String, home: String, path: String}
32
33# a question the planner asks that is not git's about a remote
34type Ask is Data:
35  Read{path: String}      # a file's text, None when there is none
36  Exec{path: String}      # whether a file is there and executable
37  Locate{path: String}    # what git says of a local path
38
39# the answer to one: a file's text, whether a binary is there, or what git
40# said of a path: its real path, the top of the checkout holding it, the
41# commit HEAD names, and whether `git status` answered and what it printed.
42# A path that has no real path, or a question IO could not answer, is a miss.
43type Answer is Data:
44  Text{got: Maybe<&2, String>}
45  Is{yes: Bool}
46  Located{abs: String, top: String, head: String, ok: Bool, status: String}
47  Miss{why: String}
48
49# one question, answered
50type Reply is Data:
51  Reply{ask: Ask, answer: Answer}
52
53# everything `ez tool run`, `install` and `upgrade` read
54type World is Data:
55  World{act: Act, target: String, want: String, here: String, env: Env, bend: String,
56    ledger: Maybe<&2, String>, lock: Maybe<&2, String>, replies: List<&2, Reply>,
57    ups: List<&2, Up.Reply>}
58
59# the act
60def act(world: World) -> Act:
61  World{a, _t, _want, _here, _env, _bend, _ledger, _lock, _rs, _ups} = world
62  a
63
64# the target asked for
65def target(world: World) -> String:
66  World{_a, t, _want, _here, _env, _bend, _ledger, _lock, _rs, _ups} = world
67  t
68
69# the file `--entry` names, "" when it names none
70def want(world: World) -> String:
71  World{_a, _t, w, _here, _env, _bend, _ledger, _lock, _rs, _ups} = world
72  w
73
74# the directory ez runs in
75def here(world: World) -> String:
76  World{_a, _t, _want, h, _env, _bend, _ledger, _lock, _rs, _ups} = world
77  h
78
79# the variables
80def env(world: World) -> Env:
81  World{_a, _t, _want, _here, e, _bend, _ledger, _lock, _rs, _ups} = world
82  e
83
84# the bend on the PATH, "" when it did not answer
85def bend(world: World) -> String:
86  World{_a, _t, _want, _here, _env, b, _ledger, _lock, _rs, _ups} = world
87  b
88
89# the project's ez.toml, None when there is none
90def ledger(world: World) -> Maybe<&2, String>:
91  World{_a, _t, _want, _here, _env, _bend, l, _lock, _rs, _ups} = world
92  l
93
94# the project's ez.lock.toml, None when there is none
95def lock(world: World) -> Maybe<&2, String>:
96  World{_a, _t, _want, _here, _env, _bend, _ledger, l, _rs, _ups} = world
97  l
98
99# every question of ours answered so far
100def replies(world: World) -> List<&2, Reply>:
101  World{_a, _t, _want, _here, _env, _bend, _ledger, _lock, rs, _ups} = world
102  rs
103
104# every one of the upgrade's questions answered so far
105def ups(world: World) -> List<&2, Up.Reply>:
106  World{_a, _t, _want, _here, _env, _bend, _ledger, _lock, _rs, us} = world
107  us
108
109def env.tool_bin(vars: Env) -> String:
110  Env{b, _x, _c, _h, _p} = vars
111  b
112
113def env.xdg_bin(vars: Env) -> String:
114  Env{_b, x, _c, _h, _p} = vars
115  x
116
117def env.xdg_cache(vars: Env) -> String:
118  Env{_b, _x, c, _h, _p} = vars
119  c
120
121def env.home(vars: Env) -> String:
122  Env{_b, _x, _c, h, _p} = vars
123  h
124
125def env.path(vars: Env) -> String:
126  Env{_b, _x, _c, _h, p} = vars
127  p
128
129# HOME
130def home(world: World) -> String:
131  env.home(env(world))
132
133# ---------------------------------------------------------------------------
134# where things go
135
136# `~/.local/bin`, when there is a HOME
137def bin.home(+home: String) -> Maybe<&2, String>:
138  Bool.pick(Maybe<&2, String>, String.is_empty(home), None{}, Some{home ++ "/.local/bin"})
139
140# `$XDG_BIN_HOME` when it is set, otherwise the one above
141def bin.xdg(+xdg: String, +home: String) -> Maybe<&2, String>:
142  Bool.pick(Maybe<&2, String>, String.is_empty(xdg), bin.home(home), Some{xdg})
143
144# where an installed command is linked: `$EZ_TOOL_BIN` when it is set,
145# otherwise `$XDG_BIN_HOME`, otherwise `$HOME/.local/bin`, an empty value
146# counting as unset; None when all three are empty (EZ-TOOL-1)
147def bin.dir(+ez: String, +xdg: String, +home: String) -> Maybe<&2, String>:
148  Bool.pick(Maybe<&2, String>, String.is_empty(ez), bin.xdg(xdg, home), Some{ez})
149
150# the link directory of a World's variables
151def bin.of(vars: Env) -> Maybe<&2, String>:
152  Env{+b, +x, _c, +h, _p} = vars
153  bin.dir(b, x, h)
154
155# the cache root: `$XDG_CACHE_HOME/ez/tool`, otherwise `$HOME/.cache/ez/tool`,
156# taken from where ez runs when it is relative, so every path the plan names
157# is absolute; None when neither variable is set
158def cache.home(+here: String, +home: String) -> Maybe<&2, String>:
159  Bool.pick(Maybe<&2, String>, String.is_empty(home), None{},
160    Some{P.anchor(here, home) ++ "/.cache/ez/tool"})
161
162def cache.dir(+here: String, +xdg: String, +home: String) -> Maybe<&2, String>:
163  Bool.pick(Maybe<&2, String>, String.is_empty(xdg), cache.home(here, home),
164    Some{P.anchor(here, xdg) ++ "/ez/tool"})
165
166# the cache root of a World
167def cache.of(world: World) -> Maybe<&2, String>:
168  World{_a, _t, _want, +here, vars, _bend, _ledger, _lock, _rs, _ups} = world
169  Env{_b, _x, +c, +h, _p} = vars
170  cache.dir(here, c, h)
171
172# a Maybe's text, "" for none
173def text.or(got: Maybe<&2, String>) -> String:
174  match got:
175    case None{}:
176      ""
177    case Some{t}:
178      t
179
180# ---------------------------------------------------------------------------
181# the project
182
183# the project's ledger as read. A project with no ez.toml pins nothing, so
184# every target is a free one; one that does not parse is its own refusal.
185def project.of(got: Maybe<&2, String>) -> M.Read:
186  match got:
187    case None{}:
188      M.Good{M.Manifest{"", "", "", "", "", "", [], []}}
189    case Some{text}:
190      M.parse(text)
191
192# the project's ledger as read
193def project(world: World) -> M.Read:
194  project.of(ledger(world))
195
196# ---------------------------------------------------------------------------
197# answers
198
199# the key a question is looked up by
200def key(ask: Ask) -> String:
201  match ask:
202    case Read{path}:
203      "read\n" ++ path
204    case Exec{path}:
205      "exec\n" ++ path
206    case Locate{path}:
207      "locate\n" ++ path
208
209# an answer looked up, or none yet
210type Heard is Data:
211  Heard{answer: Answer}
212  Open{}
213
214# this answer when it is the question's, otherwise the rest's
215def look.step(hit: Bool, answer: Answer, rest: Unit -> Heard) -> Heard:
216  match hit:
217    case True{}:
218      Heard{answer}
219    case False{}:
220      rest(Unit{})
221
222# the first answer given to a question
223def look(rs: List<&2, Reply>, +want: String) -> Heard:
224  match rs:
225    case []:
226      Open{}
227    case Reply{ask, answer} <> t:
228      look.step(String.eq(key(ask), want), answer, _u => look(t, want))
229
230# the answer to a question, or none yet
231def heard(+world: World, ask: Ask) -> Heard:
232  look(replies(world), key(ask))