src/tool/run.bend
ezx@1.4.0.0 · 373 lines · 0 definitions
1# tool/run: the interpreter for `ez tool run`, `install`, `upgrade` and
2# `sync`. It reads the directory it runs in, the variables, the bend on the
3# PATH, and the project's ez.toml and ez.lock.toml into a World, then loops:
4# it asks the planner (tool/plan.bend) what is still open, answers each
5# question by IO, and adds the answers to the World, until nothing is left
6# to ask. Then it runs the plan: the checkout laid, the binary built or
7# reused, then the program started or the link written, and the plan's end.
8# A git question is the lock interpreter's to answer (`Run.answer.ups`), a
9# file is written by its `Run.write`, and the command exits with the status
10# the planner gives the plan's end and the program's (`TP.code`), through
11# `Run.end.at`. It decides nothing; that it reads and executes faithfully is
12# EZ-TRUST-2.
13#
14# What it asks and builds on the way is captured, and a build's output is
15# shown on stderr only when the build fails. The program alone is started on
16# ez's own stdin, stdout and stderr (`Pass.run`), as `cargo run` hands the
17# terminal to the program once the build is done.
18#
19# `sync` asks the planner which pins to install (`TP.sync`) and installs
20# each in turn as `ez tool install <name>` would.
21import Base
22import snap@1.1.0.0/main.bend as R
23import ezx@1.4.0.0/src/io/file.bend as F
24import ezx@1.4.0.0/src/lock/run.bend as Run
25import ezx@1.4.0.0/src/lock/plan.bend as LP
26import ezx@1.4.0.0/src/lock/up.bend as Up
27import ezx@1.4.0.0/src/git/git.bend as Git
28import ezx@1.4.0.0/src/fetch/run.bend as FetchRun
29import ezx@1.4.0.0/src/pkg/path.bend as P
30import ezx@1.4.0.0/src/run/bend.bend as Bend
31import ezx@1.4.0.0/src/share/cap.bend as Cap
32import ezx@1.4.0.0/src/share/say.bend as Say
33import ezx@1.4.0.0/src/share/spin.bend as Spin
34import ezx@1.4.0.0/src/share/pass.bend as Pass
35import ezx@1.4.0.0/src/tool/world.bend as TW
36import ezx@1.4.0.0/src/tool/plan.bend as TP
37
38# ---------------------------------------------------------------------------
39# answering
40
41# the first line, or nothing
42def line0(ls: List<&2, String>) -> String:
43 match ls:
44 case []:
45 ""
46 case h <> _t:
47 h
48
49# git can write a `warning:` line to stderr and still exit 0, and snap
50# answers stdout and stderr together (SNAP-ANS-4), so an answer is read by the
51# shape of the line it wants, never by where that line stands.
52
53# the line found, or the walk gone on past a line of another shape
54def said.next(hit: Bool, +found: String, go: Unit -> String) -> String:
55 match hit:
56 case True{}:
57 found
58 case False{}:
59 go(Unit{})
60
61# the first line that is a commit, or ""
62def said.rev(ls: List<&2, String>) -> String:
63 match ls:
64 case []:
65 ""
66 case +h <> t:
67 said.next(Git.is_rev(h), h, _u => said.rev(t))
68
69# a line that is an absolute path, as `rev-parse --show-toplevel` prints one
70def line.abs(+line: String) -> Bool:
71 String.eq(String.take(line, 1n), "/")
72
73# the first line that is an absolute path, or ""
74def said.abs(ls: List<&2, String>) -> String:
75 match ls:
76 case []:
77 ""
78 case +h <> t:
79 said.next(line.abs(h), h, _u => said.abs(t))
80
81# what a command that succeeded answered, read only then; "" when it failed
82def said.when(ok: Bool, read: Unit -> String) -> String:
83 match ok:
84 case True{}:
85 read(Unit{})
86 case False{}:
87 ""
88
89# the commit `git rev-parse HEAD` answered, or "" when it failed
90def said.head(+out: String) -> String:
91 said.when(R.ok(out), _u => said.rev(String.lines(R.text(out))))
92
93# the top `git rev-parse --show-toplevel` answered, or "" when it failed
94def said.top(+out: String) -> String:
95 said.when(R.ok(out), _u => said.abs(String.lines(R.text(out))))
96
97# what git says of a path that has a real path: the top of its checkout,
98# HEAD, and what `git status` names, untracked files included whatever the
99# repository's own settings hide
100def located(+abs: String) -> IO(TW.Answer):
101 do IO<TW.Answer>:
102 +top : String <- R.exec(["git", "-C", abs, "rev-parse", "--show-toplevel"])
103 +head : String <- R.exec(["git", "-C", abs, "rev-parse", "HEAD"])
104 +st : String <- R.exec(["git", "-C", abs, "status", "--porcelain",
105 "--untracked-files=normal"])
106 return TW.Located{abs, said.top(top), said.head(head), R.ok(st),
107 String.trim(R.text(st))}
108
109# a path that has a real path, or why it has none
110def locate.real(ok: Bool, +path: String, +text: String) -> IO(TW.Answer):
111 match ok:
112 case False{}:
113 IO.pure(TW.Answer, TW.Miss{"ez: " ++ path ++ ": " ++ String.trim(text)})
114 case True{}:
115 located(String.trim(text))
116
117# a local path, made real, and what git says of it
118def locate(+path: String) -> IO(TW.Answer):
119 do IO<TW.Answer>:
120 +out : String <- R.exec(["realpath", path])
121 locate.real(R.ok(out), path, R.text(out))
122
123# one question answered
124def answer(ask: TW.Ask) -> IO(TW.Answer):
125 match ask:
126 case TW.Read{path}:
127 do IO<TW.Answer>:
128 m : Maybe<&2, String> <- F.read(path)
129 return TW.Text{m}
130 case TW.Exec{path}:
131 do IO<TW.Answer>:
132 +out : String <- R.exec(["test", "-x", path])
133 return TW.Is{R.ok(out)}
134 case TW.Locate{path}:
135 locate(path)
136
137# every question answered
138def answer.all(asks: List<&2, TW.Ask>) -> IO(List<&2, TW.Reply>):
139 match asks:
140 case []:
141 IO.pure(List<&2, TW.Reply>, [])
142 case +h <> t:
143 do IO<List<&2, TW.Reply>>:
144 a : TW.Answer <- answer(h)
145 rest : List<&2, TW.Reply> <- answer.all(t)
146 return TW.Reply{h, a} <> rest
147
148# the World with more answers in it
149def more(world: TW.World, rs: List<&2, TW.Reply>, us: List<&2, Up.Reply>) -> TW.World:
150 TW.World{a, t, want, here, env, bend, ledger, lock, replies, ups} = world
151 TW.World{a, t, want, here, env, bend, ledger, lock, List.append(&2, TW.Reply, replies, rs),
152 List.append(&2, Up.Reply, ups, us)}
153
154# ---------------------------------------------------------------------------
155# executing
156
157# a checkout laid: whatever was there removed, then every file, the record
158# last
159def exec.lay(layout: TP.Lay) -> IO(Unit):
160 match layout:
161 case TP.Still{}:
162 IO.pure(Unit, Unit{})
163 case TP.Lay{dir, ws}:
164 do IO<Unit>:
165 _rm : String <- R.exec(["rm", "-rf", dir])
166 Run.exec.all("", ws)
167
168# a build's output, shown on stderr when it failed, since stdout is the
169# program's, as cargo keeps its own words off it
170def compile.said(ok: Bool, +why: String, +text: String) -> IO(Unit):
171 match ok:
172 case True{}:
173 IO.pure(Unit, Unit{})
174 case False{}:
175 do IO<Unit>:
176 IO.print_err(String.trim_end(why ++ text))
177 IO.die(Unit, 1, "")
178
179# `bend <file> -o <out>` under the memory cap, with the checkout's library
180def compile(+lib: String, +file: String, +out: String) -> IO(Unit):
181 do IO<Unit>:
182 +on : Bool <- Cap.ok()
183 Cap.warn.err(on)
184 +g : String <- Cap.gb()
185 _mk : String <- R.exec(["mkdir", "-p", P.dir(out)])
186 +bin : String <- R.exec(Cap.argv(on, g, lib, ["bend", file, "-o", out]))
187 compile.said(R.ok(bin), Cap.why(bin, g), R.text(bin))
188
189# the library a build reads: an ez project's lock fetched into it as `ez
190# fetch` fetches one, with every relative path in it read from the
191# checkout; a plain repository's made, for bend to fetch its hub imports into
192def exec.lib(fetch: Bool, +top: String, +lib: String) -> IO(Unit):
193 match fetch:
194 case True{}:
195 FetchRun.fill(top, lib)
196 case False{}:
197 do IO<Unit>:
198 _mk : String <- R.exec(["mkdir", "-p", lib])
199 return Unit{}
200
201# the binary: its library filled, the file built, the record written; or the
202# one there reused
203def exec.make(making: TP.Make) -> IO(Unit):
204 match making:
205 case TP.Skip{}:
206 IO.pure(Unit, Unit{})
207 case TP.Reuse{name}:
208 IO.print_err(Say.reused(name))
209 case TP.Make{+top, +lib, fetch, file, out, at, key, +name}:
210 do IO<Unit>:
211 exec.lib(fetch, top, lib)
212 +s : Say.Spin <- Spin.hold(Say.building(name))
213 compile(lib, file, out)
214 Spin.done(s)
215 Run.write(at, key)
216
217# the lines a person is told, each on stderr
218def tell(ls: List<&2, String>) -> IO(Unit):
219 match ls:
220 case []:
221 IO.pure(Unit, Unit{})
222 case h <> t:
223 do IO<Unit>:
224 IO.print_err(h)
225 tell(t)
226
227def link.after(ok: Bool, +at: String, +text: String, says: List<&2, String>) -> IO(Unit):
228 match ok:
229 case False{}:
230 IO.die(Unit, 1, "ez: " ++ at ++ ": " ++ String.trim(text))
231 case True{}:
232 tell(says)
233
234# the program started with its words on ez's own stdin, stdout and stderr,
235# or the link written, and the status the program exited with, 0 when none
236# was started. What the program prints reaches the person as it prints it,
237# and it reads what ez would have read.
238def exec.finish(fin: TP.Finish) -> IO(U32):
239 match fin:
240 case TP.Idle{}:
241 IO.pure(U32, 0)
242 case TP.Launch{bin, args}:
243 Pass.run(List.append(&2, String, [bin], args))
244 case TP.Link{bin, +dir, +name, says}:
245 +at = dir ++ "/" ++ name
246 do IO<U32>:
247 IO.print_err(Say.linking(at))
248 _mk : String <- R.exec(["mkdir", "-p", dir])
249 +ln : String <- R.exec(["ln", "-sfn", bin, at])
250 link.after(R.ok(ln), at, R.text(ln), says)
251 return 0
252
253# a plan, executed, and how it ends: with the status the planner gives its
254# end and the program's status (`TP.code`, EZ-OUT-1)
255def exec.plan(+pl: TP.Plan) -> IO(Unit):
256 TP.Plan{l, m, f, o} = pl
257 do IO<Unit>:
258 exec.lay(l)
259 exec.make(m)
260 s : U32 <- exec.finish(f)
261 +code : U32 <- IO.pure(U32, TP.code(TP.exit(pl), s))
262 Run.end.at(U32.is_eq(code, 0), code, LP.why(o))
263
264# ---------------------------------------------------------------------------
265# the loop
266
267def loop.step(st: TP.Step, world: TW.World, go: TW.World -> IO(Unit)) -> IO(Unit):
268 match st:
269 case TP.Asking{asks, ups}:
270 do IO<Unit>:
271 rs : List<&2, TW.Reply> <- answer.all(asks)
272 us : List<&2, Up.Reply> <- Run.answer.ups("", ups)
273 go(more(world, rs, us))
274 case TP.Run{pl}:
275 exec.plan(pl)
276
277# the loop, under fuel. Every round adds an answer, since the planner never
278# asks what the World answers already, and it asks at most five rounds, so
279# the fuel is only a guard.
280def loop(fuel: Nat, +world: TW.World) -> IO(Unit):
281 match fuel:
282 case 0n:
283 IO.die(Unit, 1, "ez: ez tool kept asking questions")
284 case 1n+f:
285 loop.step(TP.step(world), world, w => loop(f, w))
286
287# a variable, "" when it is unset
288def var(name: String) -> IO(String):
289 do IO<String>:
290 r : Result<&1, &1, U32 & String, String> <- IO.get_env(name)
291 return Run.env.text(r)
292
293# the bend on the PATH, as its banner's first line. A bend that does not
294# answer is "", and its build fails on its own.
295def bend.is() -> IO(String):
296 do IO<String>:
297 +out : String <- Bend.version()
298 return Bool.pick(String, R.ok(out), line0(String.lines(String.trim(R.text(out)))), "")
299
300# the variables the tool commands read
301def env() -> IO(TW.Env):
302 do IO<TW.Env>:
303 +b : String <- var("EZ_TOOL_BIN")
304 +x : String <- var("XDG_BIN_HOME")
305 +c : String <- var("XDG_CACHE_HOME")
306 +h : String <- var("HOME")
307 +p : String <- var("PATH")
308 return TW.Env{b, x, c, h, p}
309
310# one target, run, installed or upgraded, over the project's ledger and lock,
311# building the file `want` names when it names one
312def go(
313 act: TW.Act,
314 +target: String,
315 +want: String,
316 +here: String,
317 +vars: TW.Env,
318 +bend: String,
319 +led: Maybe<&2, String>,
320 +lock: Maybe<&2, String>
321) -> IO(Unit):
322 loop(U32.to_nat(100), TW.World{act, target, want, here, vars, bend, led, lock, [], []})
323
324# `ez tool run [--entry <file>] <target> [-- args…]`, `install` and `upgrade`
325def run(act: TW.Act, +target: String, +want: String) -> IO(Unit):
326 do IO<Unit>:
327 +pwd : String <- R.exec(["pwd"])
328 +vars : TW.Env <- env()
329 +bend : String <- bend.is()
330 led : Maybe<&2, String> <- F.read("ez.toml")
331 lock : Maybe<&2, String> <- F.read("ez.lock.toml")
332 go(act, target, want, String.trim(R.text(pwd)), vars, bend, led, lock)
333
334# every name a sync installs, in turn
335def sync.all(
336 ns: List<&2, String>,
337 +here: String,
338 +vars: TW.Env,
339 +bend: String,
340 +led: Maybe<&2, String>,
341 +lock: Maybe<&2, String>
342) -> IO(Unit):
343 match ns:
344 case []:
345 IO.pure(Unit, Unit{})
346 case +h <> t:
347 do IO<Unit>:
348 go(TW.Install{}, h, "", here, vars, bend, led, lock)
349 sync.all(t, here, vars, bend, led, lock)
350
351# a sync's names installed in turn, none when it refuses, and then its end
352# (`TP.sync.outcome`)
353def sync.go(
354 +synced: TP.Sync,
355 +here: String,
356 +vars: TW.Env,
357 +bend: String,
358 +led: Maybe<&2, String>,
359 +lock: Maybe<&2, String>
360) -> IO(Unit):
361 do IO<Unit>:
362 sync.all(TP.sync.names(synced), here, vars, bend, led, lock)
363 Run.end(TP.sync.outcome(synced))
364
365# `ez tool sync`: every pin the ledger names, installed at its lock rev
366def sync() -> IO(Unit):
367 do IO<Unit>:
368 +pwd : String <- R.exec(["pwd"])
369 +vars : TW.Env <- env()
370 +bend : String <- bend.is()
371 +led : Maybe<&2, String> <- F.read("ez.toml")
372 +lock : Maybe<&2, String> <- F.read("ez.lock.toml")
373 sync.go(TP.sync(led, lock), String.trim(R.text(pwd)), vars, bend, led, lock)