src/tool/run.bend
ezx@1.10.0.0 · 379 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.3.0.0/main.bend as R
23import ezx@1.10.0.0/src/io/file.bend as F
24import ezx@1.10.0.0/src/lock/run.bend as Run
25import ezx@1.10.0.0/src/lock/plan.bend as LP
26import ezx@1.10.0.0/src/lock/up.bend as Up
27import ezx@1.10.0.0/src/git/git.bend as Git
28import ezx@1.10.0.0/src/fetch/run.bend as FetchRun
29import ezx@1.10.0.0/src/pkg/path.bend as P
30import ezx@1.10.0.0/src/run/bend.bend as Bend
31import ezx@1.10.0.0/src/share/cap.bend as Cap
32import ezx@1.10.0.0/src/share/say.bend as Say
33import ezx@1.10.0.0/src/share/spin.bend as Spin
34import ezx@1.10.0.0/src/share/pass.bend as Pass
35import ezx@1.10.0.0/src/tool/world.bend as TW
36import ezx@1.10.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)