src/ws/run.bend

ezx@1.11.0.0 · 448 lines · 47 definitions

1# ws/run: the interpreter's half of workspaces. It reads what the root search
2# asks for (ws/root.bend) and runs the hop ws/hop.bend decides, and for `ez
3# lock` it reads every ez.toml git lists under a workspace root so that
4# ws/joint.bend can join them. It decides nothing: where the root is, whether
5# to run again there, which ledgers are members and what the lock is planned
6# from are the pure modules'. That it reads and runs faithfully is
7# EZ-TRUST-2. It runs programs, so no law imports it.
8import Base
9import snap@1.4.0.0/main.bend as Sn
10import ezx@1.11.0.0/src/io/file.bend as F
11import ezx@1.11.0.0/src/share/args.bend as Args
12import ezx@1.11.0.0/src/share/pass.bend as Pass
13import ezx@1.11.0.0/src/ledger/render.bend as Rend
14import ezx@1.11.0.0/src/ws/workspace.bend as W
15import ezx@1.11.0.0/src/ws/root.bend as R
16import ezx@1.11.0.0/src/ws/hop.bend as H
17import ezx@1.11.0.0/src/ws/joint.bend as J
18import ezx@1.11.0.0/src/ws/up.bend as U
19import ezx@1.11.0.0/src/ws/pin.bend as Pin
20import ezx@1.11.0.0/src/ledger/manifest.bend as M
21import ezx@1.11.0.0/src/pkg/path.bend as P
22import ezx@1.11.0.0/src/pkg/pkg.bend as K
23import ezx@1.11.0.0/src/lock/world.bend as LW
24import ezx@1.11.0.0/src/ws/doctor.bend as Doc
25import ezx@1.11.0.0/src/ws/relock.bend as Rel
26
27# what the search found, and the top of the checkout it searched from
28type Searched is Data:
29  Searched{top: String, found: R.Found}
30
31# a directory's ez.toml, from the top of the checkout
32def ledger.path(+top: String, ds: List<&2, String>) -> String:
33  top ++ "/" ++ W.segs.text(List.append(&2, String, ds, ["ez.toml"]))
34
35# the ez.toml of every directory the search asks about, nearest first
36def read.ups(+top: String, asks: List<&2, R.Ask>) -> IO(List<&2, R.Up>):
37  match asks:
38    case []:
39      IO.pure(List<&2, R.Up>, [])
40    case R.Ask{+ds, rs} <> tl:
41      do IO<List<&2, R.Up>>:
42        tx : Maybe<&2, String> <- F.read(ledger.path(top, ds))
43        rest : List<&2, R.Up> <- read.ups(top, tl)
44        return R.Up{ds, rs, tx} <> rest
45
46# the search, once git said where ez runs. Outside a checkout there is no
47# committed ledger above, so no workspace: the directory is its own project.
48def search.at(here: Maybe<&2, List<&2, String>>, +top: String) -> IO(Searched):
49  match here:
50    case None{}:
51      IO.pure(Searched, Searched{"", R.Alone{}})
52    case Some{hs}:
53      do IO<Searched>:
54        ups : List<&2, R.Up> <- read.ups(top, R.asks(hs))
55        return Searched{top, R.root.go(ups)}
56
57# where ez runs, from what git printed, or nothing when git could not say
58def here.git(ok: Bool, +text: String) -> Maybe<&2, List<&2, String>>:
59  match ok:
60    case True{}:
61      R.here.of(String.trim(text))
62    case False{}:
63      None{}
64
65# the search for the root, from where ez runs up to the top of its checkout
66def search() -> IO(Searched):
67  do IO<Searched>:
68    +pre : String <- Sn.exec(["git", "rev-parse", "--show-prefix"])
69    +top : String <- Sn.exec(["git", "rev-parse", "--show-toplevel"])
70    search.at(here.git(Bool.and(Sn.ok(pre), Sn.ok(top)), Sn.text(pre)), String.trim(Sn.text(top)))
71
72# the program's own name, and every word after it
73def argv() -> IO(List<&2, String>):
74  do IO<List<&2, String>>:
75    xs : List<&1, String> <- IO.args()
76    return Args.copy(xs)
77
78# the same words run again at the root
79def hop.go(+dir: String, +pwd: String, xs: List<&2, String>) -> IO(U32):
80  match xs:
81    case []:
82      IO.pure(U32, 1)
83    case +prog <> rest:
84      Pass.run(H.line(dir, pwd, prog, rest))
85
86# the hop's status, as this command's
87def hop.end(zero: Bool, +code: U32) -> IO(Unit):
88  match zero:
89    case True{}:
90      IO.pure(Unit, Unit{})
91    case False{}:
92      IO.exit(code)
93
94# a run at the root, which ends as that run ended
95def hop.run(+dir: String) -> IO(Unit):
96  do IO<Unit>:
97    +pwd : String <- Sn.exec(["pwd"])
98    xs : List<&2, String> <- argv()
99    +code : U32 <- hop.go(dir, String.trim(Sn.text(pwd)), xs)
100    hop.end(U32.is_eq(code, 0), code)
101
102# a command acted on as its hop says
103def hop.at(hop: H.Hop, go: Unit -> IO(Unit)) -> IO(Unit):
104  match hop:
105    case H.Here{}:
106      go(Unit{})
107    case H.Stop{why}:
108      IO.die(Unit, 1, why)
109    case H.Hop{dir}:
110      hop.run(dir)
111
112# what the search found, acted on
113def at.searched(st: Searched, go: Unit -> IO(Unit)) -> IO(Unit):
114  Searched{+top, found} = st
115  hop.at(H.hop(top, found), go)
116
117# a whole-workspace command, `ez lock`, `ez fetch` or `ez doctor`: run where
118# it is when that is its project's root, and again at the root when it is run
119# in a member
120def at.root(go: Unit -> IO(Unit)) -> IO(Unit):
121  do IO<Unit>:
122    st : Searched <- search()
123    at.searched(st, go)
124
125# ---------------------------------------------------------------------------
126# the ledger `ez lock` is planned from
127
128# every listed ez.toml, read
129def read.found(ps: List<&2, String>) -> IO(List<&2, J.Found>):
130  match ps:
131    case []:
132      IO.pure(List<&2, J.Found>, [])
133    case +hd <> tl:
134      do IO<List<&2, J.Found>>:
135        mm : Maybe<&2, String> <- F.read(hd)
136        rest : List<&2, J.Found> <- read.found(tl)
137        return J.Found{hd, F.text_of(mm)} <> rest
138
139# the non-empty lines
140def lines(ss: List<&2, String>) -> List<&2, String>:
141  match ss:
142    case []:
143      []
144    case +hd <> tl:
145      +more = lines(tl)
146      Bool.pick(List<&2, String>, String.is_empty(hd), more, hd <> more)
147
148# the ledger `ez lock` is planned from, what an upgrade needs to write its
149# pins back, and the answers the lock reads the workspace's members from
150type Planned is Data:
151  Planned{ledger: Maybe<&2, String>, back: U.Back, seeds: List<&2, LW.Reply>}
152
153# the joint ledger, as the lock reads it, or the refusal that leaves none
154def joint.at(
155  jj: J.Joint,
156  +text: String,
157  +spec: W.Spec,
158  +fs: List<&2, J.Found>,
159  +files: List<&2, K.Source>,
160  +lock: String
161) -> IO(Planned):
162  match jj:
163    case J.Lone{}:
164      IO.pure(Planned, Planned{Some{text}, U.Alone{}, []})
165    case J.Clash{why}:
166      IO.die(Planned, 1, why)
167    case J.Joint{+mm}:
168      IO.pure(Planned, Planned{Some{Rend.show(mm)}, U.Back{text, spec, fs, lock},
169        J.seeds(J.Joint{mm}, files)})
170
171# a workspace root's ledger joined with every member's, once git listed them
172def joint.listed(
173  ok: Bool,
174  +text: String,
175  +spec: W.Spec,
176  +out: String,
177  +files: List<&2, K.Source>
178) -> IO(Planned):
179  match ok:
180    case True{}:
181      do IO<Planned>:
182        +fs : List<&2, J.Found> <- read.found(lines(String.lines(out)))
183        lk : Maybe<&2, String> <- F.read("ez.lock.toml")
184        joint.at(J.joint(text, fs, files), text, spec, fs, files, F.text_of(lk))
185    case False{}:
186      IO.die(Planned, 1,
187        "ez: a workspace is locked from the ez.toml files git tracks, and git could not list them: " ++
188          String.trim(out))
189
190# a workspace root's ledger joined with every member's
191def joint.go(+text: String, +spec: W.Spec, +files: List<&2, K.Source>) -> IO(Planned):
192  do IO<Planned>:
193    +out : String <- Sn.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", ":(glob)**/ez.toml"])
194    joint.listed(Sn.ok(out), text, spec, Sn.text(out), files)
195
196# what one ledger's text says: no workspace, read as it is; a workspace root,
197# whose members are read and joined; or a table that cannot be read, which
198# the joint ledger refuses
199def joint.read(rd: W.Read, +text: String, +files: List<&2, K.Source>) -> IO(Planned):
200  match rd:
201    case W.Lone{}:
202      IO.pure(Planned, Planned{Some{text}, U.Alone{}, []})
203    case W.Bad{why}:
204      IO.die(Planned, 1, why)
205    case W.Root{+spec}:
206      joint.go(text, spec, files)
207
208# the ledger `ez lock` is planned from: ez.toml as it is, or, at a workspace
209# root, the joint ledger of the root and every member. `files` are the
210# committed `.bend` files, which a member's package is walked over.
211def ledger(led: Maybe<&2, String>, +files: List<&2, K.Source>) -> IO(Planned):
212  match led:
213    case None{}:
214      IO.pure(Planned, Planned{None{}, U.Alone{}, []})
215    case Some{+text}:
216      joint.read(W.spec.of(text), text, files)
217
218# ---------------------------------------------------------------------------
219# a member's own commands
220
221# an environment variable's text; unset, ""
222def env.text(rr: Result<&1, &1, U32 & String, String>) -> String:
223  match rr:
224    case Fail{_e}:
225      ""
226    case Done{ss}:
227      String.trim(ss)
228
229# BEND_LIB as the environment has it, "" for unset
230def env.lib() -> IO(String):
231  do IO<String>:
232    rr : Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LIB")
233    return env.text(rr)
234
235# the same words run again where they are, with BEND_LIB set
236def lib.go(+at: String, +pwd: String, xs: List<&2, String>) -> IO(U32):
237  match xs:
238    case []:
239      IO.pure(U32, 1)
240    case +prog <> rest:
241      Pass.run(H.lib.line(at, pwd, prog, rest))
242
243# a command acted on as the library decision says: where it is, or again
244# where it is with BEND_LIB set to the root's
245def lib.at(lib: H.Lib, go: Unit -> IO(Unit)) -> IO(Unit):
246  match lib:
247    case H.Keep{}:
248      go(Unit{})
249    case H.Set{+at}:
250      do IO<Unit>:
251        +pwd : String <- Sn.exec(["pwd"])
252        xs : List<&2, String> <- argv()
253        +code : U32 <- lib.go(at, String.trim(Sn.text(pwd)), xs)
254        hop.end(U32.is_eq(code, 0), code)
255
256# what the search found, acted on for a member's own command
257def in.searched(st: Searched, +env: String, go: Unit -> IO(Unit)) -> IO(Unit):
258  Searched{+top, found} = st
259  lib.at(H.lib(top, found, env), go)
260
261# a command that acts on a member's own ledger and entry, `ez add`, `ez
262# remove`, `ez check`, `ez build`, `ez run` and `ez publish`: run where it is,
263# with the root's library when it is run in a member
264def in.member(go: Unit -> IO(Unit)) -> IO(Unit):
265  do IO<Unit>:
266    st : Searched <- search()
267    +env : String <- env.lib()
268    in.searched(st, env, go)
269
270# every listed ez.toml under a directory, read
271def read.under(+dir: String, ps: List<&2, String>) -> IO(List<&2, J.Found>):
272  match ps:
273    case []:
274      IO.pure(List<&2, J.Found>, [])
275    case +hd <> tl:
276      do IO<List<&2, J.Found>>:
277        mm : Maybe<&2, String> <- F.read(dir ++ "/" ++ hd)
278        rest : List<&2, J.Found> <- read.under(dir, tl)
279        return J.Found{hd, F.text_of(mm)} <> rest
280
281# the joint ledger's dependencies, or none when it has none
282def pins.joint(jj: J.Joint) -> List<&2, M.Dep>:
283  match jj:
284    case J.Joint{mm}:
285      M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = mm
286      ds
287    case _:
288      []
289
290# every listed file under a directory, read, as the walk takes it
291def files.under(+dir: String, ps: List<&2, String>) -> IO(List<&2, K.Source>):
292  match ps:
293    case []:
294      IO.pure(List<&2, K.Source>, [])
295    case +hd <> tl:
296      do IO<List<&2, K.Source>>:
297        mm : Maybe<&2, String> <- F.read(dir ++ "/" ++ hd)
298        rest : List<&2, K.Source> <- files.under(dir, tl)
299        return K.Source{hd, F.text_of(mm)} <> rest
300
301# every dependency of the workspace a directory is in, as its root reads them
302def pins.at(+dir: String) -> IO(List<&2, M.Dep>):
303  do IO<List<&2, M.Dep>>:
304    mm : Maybe<&2, String> <- F.read(dir ++ "/ez.toml")
305    +out : String <- Sn.exec(["git", "-C", dir, "-c", "core.quotepath=off", "ls-files", "--",
306      ":(glob)**/ez.toml"])
307    fs : List<&2, J.Found> <- read.under(dir, lines(String.lines(Sn.text(out))))
308    +bend : String <- Sn.exec(["git", "-C", dir, "-c", "core.quotepath=off", "ls-files", "--", "*.bend"])
309    files : List<&2, K.Source> <- files.under(dir, lines(String.lines(Sn.text(bend))))
310    return pins.joint(J.joint(F.text_of(mm), fs, files))
311
312# the ref, once the workspace's pins are known
313def ref.held(rr: Pin.Ref) -> IO(String):
314  match rr:
315    case Pin.Given{ref}:
316      IO.pure(String, ref)
317    case Pin.Held{why}:
318      IO.die(String, 1, why)
319
320# the ref an add goes on with, in what the search found
321def ref.found(+top: String, found: R.Found, +home: String, +target: String, +ref: String) -> IO(String):
322  match found:
323    case R.Within{+root, +rel}:
324      do IO<String>:
325        +pins : List<&2, M.Dep> <- pins.at(P.join(top, W.segs.text(root)))
326        ref.held(Pin.ref(pins, W.segs.text(rel), home, target, ref))
327    case _:
328      IO.pure(String, ref)
329
330# and once the search was made
331def add.searched(st: Searched, +home: String, +target: String, +ref: String) -> IO(String):
332  Searched{+top, found} = st
333  ref.found(top, found, home, target, ref)
334# the ref `ez add <target> [ref]` goes on with: in a workspace, the pin the
335# workspace already records for the source (EZ-WS-4)
336def add.ref(+home: String, +target: String, +ref: String) -> IO(String):
337  do IO<String>:
338    st : Searched <- search()
339    add.searched(st, home, target, ref)
340
341
342# ---------------------------------------------------------------------------
343# what `ez add` and `ez remove` read of the root
344
345# every listed file under a directory, read, as the lock takes it
346def sources.under(+dir: String, ps: List<&2, String>) -> IO(List<&2, LW.Source>):
347  match ps:
348    case []:
349      IO.pure(List<&2, LW.Source>, [])
350    case +hd <> tl:
351      do IO<List<&2, LW.Source>>:
352        mm : Maybe<&2, String> <- F.read(dir ++ "/" ++ hd)
353        rest : List<&2, LW.Source> <- sources.under(dir, tl)
354        return LW.Source{hd, F.text_of(mm)} <> rest
355
356# the `.bend` files git lists under a directory, once it listed them
357def listing.listed(ok: Bool, +dir: String, +out: String) -> IO(LW.Listing):
358  match ok:
359    case True{}:
360      do IO<LW.Listing>:
361        fs : List<&2, LW.Source> <- sources.under(dir, lines(String.lines(out)))
362        return LW.Listed{fs}
363    case False{}:
364      IO.pure(LW.Listing, LW.Unlisted{
365        "ez: ez lock reads the .bend files git tracks, and git could not list them: " ++ String.trim(out)})
366
367# every `.bend` file git tracks under a directory, with its text, each path
368# from it, as `ez lock` run there lists them
369def listing.at(+dir: String) -> IO(LW.Listing):
370  do IO<LW.Listing>:
371    +out : String <- Sn.exec(["git", "-C", dir, "-c", "core.quotepath=off", "ls-files", "--", "*.bend"])
372    listing.listed(Sn.ok(out), dir, Sn.text(out))
373
374# what a command run at `rel` from the root at `at` reads of the root: its
375# ledger, every ez.toml and `.bend` file git lists under it, and its lock
376def rooted(+at: String, rel: List<&2, String>) -> IO(Rel.Rooted):
377  do IO<Rel.Rooted>:
378    mm : Maybe<&2, String> <- F.read(at ++ "/ez.toml")
379    +out : String <- Sn.exec(["git", "-C", at, "-c", "core.quotepath=off", "ls-files", "--",
380      ":(glob)**/ez.toml"])
381    fs : List<&2, J.Found> <- read.under(at, lines(String.lines(Sn.text(out))))
382    ls : LW.Listing <- listing.at(at)
383    lk : Maybe<&2, String> <- F.read(at ++ "/ez.lock.toml")
384    return Rel.Rooted{at, rel, F.text_of(mm), fs, ls, F.text_of(lk)}
385
386# where `ez add` or `ez remove` runs, in what the search found
387def ws.found(+top: String, found: R.Found) -> IO(Rel.Ws):
388  match found:
389    case R.Within{root, rel}:
390      do IO<Rel.Ws>:
391        rt : Rel.Rooted <- rooted(P.join(top, W.segs.text(root)), rel)
392        return Rel.Inside{rt}
393    case _:
394      IO.pure(Rel.Ws, Rel.Own{})
395
396# and once the search was made
397def ws.searched(st: Searched) -> IO(Rel.Ws):
398  Searched{+top, found} = st
399  ws.found(top, found)
400
401# where `ez add` or `ez remove` runs: in a project of its own, or in a
402# workspace, with what it reads of the root (EZ-WS-10)
403def relock.ws() -> IO(Rel.Ws):
404  do IO<Rel.Ws>:
405    st : Searched <- search()
406    ws.searched(st)
407
408# the lock, once the search was made
409def lock.searched(st: Searched) -> String:
410  Searched{+top, found} = st
411  H.lock(top, found)
412
413# the lock a member's own command reads: the root's, in a workspace
414def lock.path() -> IO(String):
415  do IO<String>:
416    st : Searched <- search()
417    return lock.searched(st)
418
419# ---------------------------------------------------------------------------
420# `ez doctor` at a workspace root
421
422# a workspace root's ledger and every member's, read for the doctor
423def doctor.root(+text: String) -> IO(Doc.Doctored):
424  do IO<Doc.Doctored>:
425    +out : String <- Sn.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", ":(glob)**/ez.toml"])
426    +fs : List<&2, J.Found> <- read.found(lines(String.lines(Sn.text(out))))
427    +bend : String <- Sn.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", "*.bend"])
428    files : List<&2, K.Source> <- files.under(".", lines(String.lines(Sn.text(bend))))
429    return Doc.joint(J.joint(text, fs, files), text, fs)
430
431# what one ledger's text says, for the doctor
432def doctor.read(rd: W.Read, +text: String) -> IO(Doc.Doctored):
433  match rd:
434    case W.Lone{}:
435      IO.pure(Doc.Doctored, Doc.Doctored{Some{text}, Doc.Alone{}})
436    case W.Bad{why}:
437      IO.pure(Doc.Doctored, Doc.Doctored{Some{text}, Doc.Back{text, [], why}})
438    case W.Root{_spec}:
439      doctor.root(text)
440
441# the ledger `ez doctor` judges: ez.toml as it is, or, at a workspace root,
442# the joint ledger of the root and every member
443def doctor.ledger(led: Maybe<&2, String>) -> IO(Doc.Doctored):
444  match led:
445    case None{}:
446      IO.pure(Doc.Doctored, Doc.Doctored{None{}, Doc.Alone{}})
447    case Some{+text}:
448      doctor.read(W.spec.of(text), text)