src/tool/run.bend

ezx@1.11.0.0 · 379 lines · 34 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.4.0.0/main.bend as R
23import ezx@1.11.0.0/src/io/file.bend as F
24import ezx@1.11.0.0/src/lock/run.bend as Run
25import ezx@1.11.0.0/src/lock/plan.bend as LP
26import ezx@1.11.0.0/src/lock/up.bend as Up
27import ezx@1.11.0.0/src/git/git.bend as Git
28import ezx@1.11.0.0/src/fetch/run.bend as FetchRun
29import ezx@1.11.0.0/src/pkg/path.bend as P
30import ezx@1.11.0.0/src/run/bend.bend as Bend
31import ezx@1.11.0.0/src/share/cap.bend as Cap
32import ezx@1.11.0.0/src/share/say.bend as Say
33import ezx@1.11.0.0/src/share/spin.bend as Spin
34import ezx@1.11.0.0/src/share/pass.bend as Pass
35import ezx@1.11.0.0/src/tool/world.bend as TW
36import ezx@1.11.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.Plain{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 rounds the loop may take. The planner asks at most five rounds, so this
278# is only a guard against a planner that keeps asking, and running out of it
279# dies loudly.
280def rounds() -> Nat:
281  U32.to_nat(100)
282
283# the loop, under fuel. Every round adds an answer, since the planner never
284# asks what the World answers already, and it asks at most five rounds, so
285# the fuel is only a guard.
286def loop(fuel: Nat, +world: TW.World) -> IO(Unit):
287  match fuel:
288    case 0n:
289      IO.die(Unit, 1, "ez: ez tool kept asking questions")
290    case 1n+f:
291      loop.step(TP.step(world), world, w => loop(f, w))
292
293# a variable, "" when it is unset
294def var(name: String) -> IO(String):
295  do IO<String>:
296    r : Result<&1, &1, U32 & String, String> <- IO.get_env(name)
297    return Run.env.text(r)
298
299# the bend on the PATH, as its banner's first line. A bend that does not
300# answer is "", and its build fails on its own.
301def bend.is() -> IO(String):
302  do IO<String>:
303    +out : String <- Bend.version()
304    return Bool.pick(String, R.ok(out), line0(String.lines(String.trim(R.text(out)))), "")
305
306# the variables the tool commands read
307def env() -> IO(TW.Env):
308  do IO<TW.Env>:
309    +b : String <- var("EZ_TOOL_BIN")
310    +x : String <- var("XDG_BIN_HOME")
311    +c : String <- var("XDG_CACHE_HOME")
312    +h : String <- var("HOME")
313    +p : String <- var("PATH")
314    return TW.Env{b, x, c, h, p}
315
316# one target, run, installed or upgraded, over the project's ledger and lock,
317# building the file `want` names when it names one
318def go(
319  act: TW.Act,
320  +target: String,
321  +want: String,
322  +here: String,
323  +vars: TW.Env,
324  +bend: String,
325  +led: Maybe<&2, String>,
326  +lock: Maybe<&2, String>
327) -> IO(Unit):
328  loop(rounds(), TW.World{act, target, want, here, vars, bend, led, lock, [], []})
329
330# `ez tool run [--entry <file>] <target> [-- args…]`, `install` and `upgrade`
331def run(act: TW.Act, +target: String, +want: String) -> IO(Unit):
332  do IO<Unit>:
333    +pwd : String <- R.exec(["pwd"])
334    +vars : TW.Env <- env()
335    +bend : String <- bend.is()
336    led : Maybe<&2, String> <- F.read("ez.toml")
337    lock : Maybe<&2, String> <- F.read("ez.lock.toml")
338    go(act, target, want, String.trim(R.text(pwd)), vars, bend, led, lock)
339
340# every name a sync installs, in turn
341def sync.all(
342  ns: List<&2, String>,
343  +here: String,
344  +vars: TW.Env,
345  +bend: String,
346  +led: Maybe<&2, String>,
347  +lock: Maybe<&2, String>
348) -> IO(Unit):
349  match ns:
350    case []:
351      IO.pure(Unit, Unit{})
352    case +h <> t:
353      do IO<Unit>:
354        go(TW.Install{}, h, "", here, vars, bend, led, lock)
355        sync.all(t, here, vars, bend, led, lock)
356
357# a sync's names installed in turn, none when it refuses, and then its end
358# (`TP.sync.outcome`)
359def sync.go(
360  +synced: TP.Sync,
361  +here: String,
362  +vars: TW.Env,
363  +bend: String,
364  +led: Maybe<&2, String>,
365  +lock: Maybe<&2, String>
366) -> IO(Unit):
367  do IO<Unit>:
368    sync.all(TP.sync.names(synced), here, vars, bend, led, lock)
369    Run.end(TP.sync.outcome(synced))
370
371# `ez tool sync`: every pin the ledger names, installed at its lock rev
372def sync() -> IO(Unit):
373  do IO<Unit>:
374    +pwd : String <- R.exec(["pwd"])
375    +vars : TW.Env <- env()
376    +bend : String <- bend.is()
377    +led : Maybe<&2, String> <- F.read("ez.toml")
378    +lock : Maybe<&2, String> <- F.read("ez.lock.toml")
379    sync.go(TP.sync(led, lock), String.trim(R.text(pwd)), vars, bend, led, lock)