src/lock/run.bend
ezx@1.10.0.0 · 754 lines · 0 definitions
1# lock/run: the interpreter for `ez lock`. It reads the ledger and the
2# committed sources into a World, then loops: it asks the planner
3# (lock/plan.bend) what is still open, answers each question by IO, and adds
4# the answers to the World, until nothing is left to ask. Then it runs the
5# planner's plan. It decides nothing: which packages to read, whether their
6# bytes are the ones the ledger names, where an upgrade moves a pin, and what
7# is written are the planner's. That it reads and executes faithfully is
8# EZ-TRUST-2. It answers a name's question by asking the hub once, and the
9# lock's by reading ez.lock.toml. Under `--upgrade` it also answers the
10# upgrade's questions (lock/up.bend): what git prints for refs, the default
11# branch and tags, ancestry, checkouts at a rev, the hub's manifest for a
12# moved hash, `.gitignore`, and the project's sources. A new checkout's package is walked
13# from its entry here, by `K.pkg_of`, which is interpreter code under
14# EZ-TRUST-2 until `ez add` is converted.
15import Base
16import snap@1.3.0.0/main.bend as R
17import ezx@1.10.0.0/src/io/file.bend as F
18import ezx@1.10.0.0/src/hub/hub.bend as Web
19import ezx@1.10.0.0/src/hub/get.bend as Net
20import ezx@1.10.0.0/src/git/git.bend as Git
21import ezx@1.10.0.0/src/git/exec.bend as GX
22import ezx@1.10.0.0/src/pkg/pkg.bend as K
23import ezx@1.10.0.0/src/pkg/path.bend as P
24import ezx@1.10.0.0/src/share/say.bend as Say
25import ezx@1.10.0.0/src/share/spin.bend as Spin
26import ezx@1.10.0.0/src/lock/lock.bend as L
27import ezx@1.10.0.0/src/lock/world.bend as W
28import ezx@1.10.0.0/src/lock/up.bend as Up
29import ezx@1.10.0.0/src/lock/plan.bend as Plan
30import ezx@1.10.0.0/src/ws/run.bend as WsRun
31import ezx@1.10.0.0/src/ws/up.bend as WsUp
32import ezx@1.10.0.0/src/ws/joint.bend as J
33
34# ---------------------------------------------------------------------------
35# the World's first fields
36
37# an environment variable's text; unset, ""
38def env.text(res: Result<&1, &1, U32 & String, String>) -> String:
39 match res:
40 case Fail{_e}:
41 ""
42 case Done{s}:
43 s
44
45# where packages live, which is with the project rather than in ~/.bend/lib
46def lib.or(+dir: String) -> String:
47 Bool.pick(String, String.is_empty(dir), ".ez/lib", dir)
48
49# the BEND_LIB this project pins
50def bend.lib() -> IO(String):
51 do IO<String>:
52 r : Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LIB")
53 return lib.or(String.trim(env.text(r)))
54
55# the non-empty lines
56def nonempty(ss: List<&2, String>) -> List<&2, String>:
57 match ss:
58 case []:
59 []
60 case +h <> t:
61 +more = nonempty(t)
62 Bool.pick(List<&2, String>, String.is_empty(h), more, h <> more)
63
64# the ledger's text, or None when there is no ez.toml
65def read.ledger() -> IO(Maybe<&2, String>):
66 F.read("ez.toml")
67
68# every listed path, read
69def read.all(ps: List<&2, String>) -> IO(List<&2, W.Source>):
70 match ps:
71 case []:
72 IO.pure(List<&2, W.Source>, [])
73 case +h <> t:
74 do IO<List<&2, W.Source>>:
75 m : Maybe<&2, String> <- F.read(h)
76 rest : List<&2, W.Source> <- read.all(t)
77 return W.Source{h, F.text_of(m)} <> rest
78
79# what git listed, or why it could not
80def listed(ok: Bool, +text: String) -> IO(W.Listing):
81 match ok:
82 case True{}:
83 do IO<W.Listing>:
84 fs : List<&2, W.Source> <- read.all(nonempty(String.lines(text)))
85 return W.Listed{fs}
86 case False{}:
87 IO.pure(W.Listing, W.Unlisted{
88 "ez: ez lock reads the .bend files git tracks, and git could not list them: " ++
89 String.trim(text)})
90
91# every `.bend` file git tracks under the current directory, with its text.
92# Untracked files are not a clone's, so they are not the lock's.
93def read.listing() -> IO(W.Listing):
94 do IO<W.Listing>:
95 +out : String <- R.exec(["git", "-c", "core.quotepath=off", "ls-files",
96 "--", "*.bend"])
97 listed(R.ok(out), R.text(out))
98
99# ---------------------------------------------------------------------------
100# answering one question
101
102# every text a fetch answered, or the first miss
103def gots(+how: W.How, +manifest: String, gs: List<&2, Web.Got>, acc: List<&2, String>) -> W.Answer:
104 match gs:
105 case []:
106 W.Got{how, manifest, List.reverse(&2, String, acc)}
107 case g <> t:
108 match g:
109 case Web.Miss{why}:
110 W.Miss{"ez: " ++ why}
111 case Web.Have{body}:
112 gots(how, manifest, t, body <> acc)
113
114# every file of a hub package, fetched and checked against its sum
115def hub.all(+hub: String, +hash: String, fs: List<&2, K.Item>) -> IO(List<&2, Web.Got>):
116 match fs:
117 case []:
118 IO.pure(List<&2, Web.Got>, [])
119 case +h <> t:
120 do IO<List<&2, Web.Got>>:
121 g : Web.Got <- Net.fetch(L.url_of(hub, hash, K.file.at(h)), K.file.sum(h))
122 rest : List<&2, Web.Got> <- hub.all(hub, hash, t)
123 return g <> rest
124
125# a hub package's files, once its manifest arrived
126def hub.got(got: Web.Got, +hub: String, +hash: String) -> IO(W.Answer):
127 match got:
128 case Web.Miss{why}:
129 IO.pure(W.Answer, W.Miss{why})
130 case Web.Have{+body}:
131 do IO<W.Answer>:
132 gs : List<&2, Web.Got> <- hub.all(hub, hash, L.manifest.files(String.lines(body)))
133 return gots(W.Served{}, body, gs, [])
134
135# a hub package: its manifest, then every file it names
136def answer.hub(+hub: String, +hash: String) -> IO(W.Answer):
137 do IO<W.Answer>:
138 +s : Say.Spin <- Spin.hold(Say.dep.hub(hash))
139 g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash))
140 a : W.Answer <- hub.got(g, hub, hash)
141 Spin.done(s)
142 return a
143
144# every file a tree's manifest names, read from beside it. A file that is not
145# there reads as "", which does not hash to its sum.
146def disk.all(+dir: String, fs: List<&2, K.Item>) -> IO(List<&2, String>):
147 match fs:
148 case []:
149 IO.pure(List<&2, String>, [])
150 case +h <> t:
151 do IO<List<&2, String>>:
152 m : Maybe<&2, String> <- F.read(P.join(dir, K.file.at(h)))
153 rest : List<&2, String> <- disk.all(dir, t)
154 return F.text_of(m) <> rest
155
156# a tree under BEND_LIB, as it is
157def answer.lib(+dir: String, +manifest: String) -> IO(W.Answer):
158 do IO<W.Answer>:
159 ss : List<&2, String> <- disk.all(dir, L.manifest.files(String.lines(manifest)))
160 return W.Got{W.Lib{}, manifest, ss}
161
162# a checkout that could not be made or read is removed, and the question is
163# answered with why
164def clone.miss(+work: String, +why: String) -> IO(W.Answer):
165 do IO<W.Answer>:
166 _rm : String <- R.exec(["rm", "-rf", work])
167 return W.Miss{why}
168
169# a command of a clone that worked goes on with the clone; one that failed
170# ends it
171def clone.ran(ok: Bool, +work: String, +why: String, go: Unit -> IO(W.Answer)) -> IO(W.Answer):
172 match ok:
173 case True{}:
174 go(Unit{})
175 case False{}:
176 clone.miss(work, why)
177
178# one git command of a clone, and the rest of the clone when it worked
179def clone.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(W.Answer)) -> IO(W.Answer):
180 do IO<W.Answer>:
181 +out : String <- R.exec(args)
182 clone.ran(R.ok(out), work, "ez: git " ++ what ++ ": " ++ R.text(out), go)
183
184# the checkout's package, walked from its entry the way `ez add` walked it
185def clone.walk(+work: String, +entry: String, +nar: String) -> IO(W.Answer):
186 do IO<W.Answer>:
187 +p : K.Pkg <- K.pkg_in(work, entry)
188 +fs : List<&2, K.Item> <- IO.pure(List<&2, K.Item>, K.pkg.files(p))
189 ss : List<&2, String> <- disk.all(K.pkg.root(p), fs)
190 _rm : String <- R.exec(["rm", "-rf", work])
191 return W.Got{W.Clone{nar}, K.manifest_of(fs), ss}
192
193# the entry has to be in the checkout for there to be a package to walk
194def clone.entry(ok: Bool, +work: String, +entry: String, +url: String, +rev: String, +nar: String) -> IO(W.Answer):
195 match ok:
196 case True{}:
197 clone.walk(work, entry, nar)
198 case False{}:
199 clone.miss(work, "ez: " ++ entry ++ " is not in " ++ url ++ " at " ++ rev)
200
201# the checkout, weighed and walked
202def clone.weigh(+work: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer):
203 do IO<W.Answer>:
204 _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
205 +s : Say.Spin <- Spin.hold(Say.hashing(rev))
206 +nar : String <- GX.nar(work)
207 Spin.done(s)
208 +ck : String <- R.exec(["test", "-f", P.join(work, entry)])
209 clone.entry(R.ok(ck), work, entry, url, rev, nar)
210
211# a git package whose tree is not under BEND_LIB: a shallow clone at the rev
212# the ledger pins, the way `ez add` and `ez fetch` clone, from the source the
213# question names, which the planner anchored at the project root. Nothing is
214# laid here; the plan lays the tree once it has checked it.
215def answer.clone(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer):
216 +work = lib ++ "/.work-" ++ hash
217 do IO<W.Answer>:
218 _rm : String <- R.exec(["rm", "-rf", work])
219 _mk : String <- R.exec(["mkdir", "-p", work])
220 +s : Say.Spin <- Spin.hold(Say.dep.git(Say.label(hash, entry), url))
221 a : W.Answer <- clone.run(work, "init", ["git", "init", "-q", work], _u =>
222 clone.run(work, "fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", url, rev], _v =>
223 clone.run(work, "checkout", ["git", "-C", work, "checkout", "-q", rev], _w =>
224 clone.weigh(work, url, rev, entry))))
225 Spin.done(s)
226 return a
227
228# a tree already under BEND_LIB is read; a missing one is cloned
229def git.seen(
230 got: Maybe<&2, String>,
231 +lib: String,
232 +hash: String,
233 +url: String,
234 +rev: String,
235 +entry: String
236) -> IO(W.Answer):
237 match got:
238 case Some{text}:
239 answer.lib(lib ++ "/" ++ hash, text)
240 case None{}:
241 answer.clone(lib, hash, url, rev, entry)
242
243# a git package
244def answer.git(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer):
245 do IO<W.Answer>:
246 m : Maybe<&2, String> <- F.read(lib ++ "/" ++ hash ++ "/manifest")
247 git.seen(m, lib, hash, url, rev, entry)
248
249# a package, from where its source says
250def answer.pkg(+lib: String, +hash: String, src: L.Src, +hub: String) -> IO(W.Answer):
251 match src:
252 case L.Hub{}:
253 answer.hub(hub, hash)
254 case L.Git{url, rev, entry, _root, _nar, _tag}:
255 answer.git(lib, hash, url, rev, entry)
256 case L.Member{path, _entry}:
257 IO.pure(W.Answer, W.Miss{"ez: the workspace member at " ++ path ++ " was not walked"})
258
259# what the hub says a name names, as it said it; the planner reads the hash
260# out of it
261def said(got: Web.Got) -> W.Answer:
262 match got:
263 case Web.Miss{why}:
264 W.Miss{"ez: " ++ why}
265 case Web.Have{body}:
266 W.Said{body}
267
268# a name, asked of the hub once: `GET <hub>/name/<nv>`
269def answer.name(+hub: String, +nv: String) -> IO(W.Answer):
270 do IO<W.Answer>:
271 +s : Say.Spin <- Spin.hold(Say.dep.hub(nv))
272 g : Web.Got <- Net.fetch(hub ++ "/name/" ++ nv, "")
273 Spin.done(s)
274 return said(g)
275
276# the lock being rewritten, "" when there is none
277def answer.lock() -> IO(W.Answer):
278 do IO<W.Answer>:
279 m : Maybe<&2, String> <- F.read(W.lockfile())
280 return W.Locked{F.text_of(m)}
281
282# one question, answered
283def answer(+lib: String, ask: W.Ask) -> IO(W.Answer):
284 match ask:
285 case W.Pkg{+hash, src, +hub}:
286 answer.pkg(lib, hash, src, hub)
287 case W.Name{+nv, +hub}:
288 answer.name(hub, nv)
289 case W.Lock{}:
290 answer.lock()
291
292# every question, answered
293def answer.all(+lib: String, asks: List<&2, W.Ask>) -> IO(List<&2, W.Reply>):
294 match asks:
295 case []:
296 IO.pure(List<&2, W.Reply>, [])
297 case +h <> t:
298 do IO<List<&2, W.Reply>>:
299 a : W.Answer <- answer(lib, h)
300 rest : List<&2, W.Reply> <- answer.all(lib, t)
301 return W.Reply{h, a} <> rest
302
303# ---------------------------------------------------------------------------
304# answering the upgrade's questions
305
306# what git printed, and whether it succeeded, kept as it was
307def git.said(+what: String, args: List<&2, String>) -> IO(Up.Answer):
308 do IO<Up.Answer>:
309 +s : Say.Spin <- Spin.hold(what)
310 +out : String <- R.exec(args)
311 Spin.done(s)
312 return Up.Printed{R.ok(out), R.text(out)}
313
314# The upgrade's questions, and `ez add`'s and `ez fetch`'s, name the source
315# as git is to read it, anchored at the project by their planners
316# (`Up.anchor`, `P.Query`), so each is handed to git as it is.
317
318# the refs a ref could name: a tag, its peeled form, and a branch, all at
319# once, because which of them exists is the question
320def answer.refs(+url: String, +ref: String) -> IO(Up.Answer):
321 git.said(Say.resolving.at(url), ["git", "ls-remote", url, "refs/tags/" ++ ref,
322 "refs/tags/" ++ ref ++ "^{}", "refs/heads/" ++ ref])
323
324# HEAD on the remote, with the branch it is a symref to
325def answer.head(+url: String) -> IO(Up.Answer):
326 git.said(Say.resolving.at(url), ["git", "ls-remote", "--symref", url, "HEAD"])
327
328# every tag on the remote
329def answer.tags(+url: String) -> IO(Up.Answer):
330 git.said(Say.resolving.at(url), ["git", "ls-remote", "--tags", url])
331
332# whether `tip` descends from `pin`, asked of a blobless bare clone
333def answer.above(+lib: String, +url: String, +pin: String, +tip: String) -> IO(Up.Answer):
334 do IO<Up.Answer>:
335 yes : Bool <- GX.descendant.at(lib ++ "/.work-above", url, pin, tip)
336 return Up.Onward{yes}
337
338# the checkout's package, walked from its entry the way `ez add` walks it, with
339# the directory its paths are written from, against the repo
340def tree.walk(+work: String, +at: String, +nar: String) -> IO(Up.Answer):
341 do IO<Up.Answer>:
342 +p : K.Pkg <- K.pkg_in(work, at)
343 +fs : List<&2, K.Item> <- IO.pure(List<&2, K.Item>, K.pkg.files(p))
344 ss : List<&2, String> <- disk.all(K.pkg.root(p), fs)
345 +rt : String <- IO.pure(String, Git.root.rel(P.join(work, at), K.pkg.root(p), at))
346 _rm : String <- R.exec(["rm", "-rf", work])
347 return Up.Checkout{nar, rt, K.manifest_of(fs), ss}
348
349# the entry has to be in the checkout for there to be a package to walk
350def tree.entry(ok: Bool, +work: String, +at: String, +url: String, +rev: String, +nar: String) -> IO(Up.Answer):
351 match ok:
352 case True{}:
353 tree.walk(work, at, nar)
354 case False{}:
355 do IO<Up.Answer>:
356 _rm : String <- R.exec(["rm", "-rf", work])
357 return Up.Miss{"ez: " ++ at ++ " is not in " ++ url ++ " at " ++ rev}
358
359# a checkout at a rev: cloned, its `.git` removed, weighed, and walked. An
360# entry that was not recorded is read from the checkout. Nothing is laid and
361# nothing is noted: a tree the upgrade moves to is laid by the plan, once the
362# upgrade and the lock both succeed.
363def answer.tree(+lib: String, +url: String, +rev: String, +entry: String) -> IO(Up.Answer):
364 +work = lib ++ "/.work-" ++ rev
365 do IO<Up.Answer>:
366 _rm : String <- R.exec(["rm", "-rf", work])
367 _mk : String <- R.exec(["mkdir", "-p", work])
368 GX.clone(work, url, rev)
369 +at : String <- Git.entry.for(Bool.not(String.is_empty(entry)), entry, work)
370 _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
371 +s : Say.Spin <- Spin.hold(Say.hashing(rev))
372 +nar : String <- GX.nar(work)
373 Spin.done(s)
374 +ck : String <- R.exec(["test", "-f", P.join(work, at)])
375 tree.entry(R.ok(ck), work, at, url, rev, nar)
376
377# a commit's NAR hash, for a tool
378def answer.weigh(+url: String, +rev: String) -> IO(Up.Answer):
379 do IO<Up.Answer>:
380 +nar : String <- GX.weigh(url, rev)
381 return Up.Weighed{nar}
382
383# the hub's manifest for a hash a dependency moved to
384def answer.hubhas(+hub: String, +hash: String) -> IO(Up.Answer):
385 do IO<Up.Answer>:
386 g : Web.Got <- Net.fetch(L.url_of(hub, hash, "manifest"), L.want(hash))
387 return Up.HubSaid{g}
388
389# the ignore file, "" when it is missing
390def answer.ignore() -> IO(Up.Answer):
391 do IO<Up.Answer>:
392 m : Maybe<&2, String> <- F.read(".gitignore")
393 return Up.Plain{F.text_of(m)}
394
395# one source in front of the rest, or the first that could not be read
396def found.put(got: Maybe<&2, String>, +at: String, rest: Up.Answer) -> Up.Answer:
397 match got:
398 case None{}:
399 Up.Miss{"ez: " ++ at ++ " could not be read"}
400 case Some{text}:
401 match rest:
402 case Up.Files{fs}:
403 Up.Files{Up.Found{at, text} <> fs}
404 case _:
405 rest
406
407# every listed source, read
408def found.all(ps: List<&2, String>) -> IO(Up.Answer):
409 match ps:
410 case []:
411 IO.pure(Up.Answer, Up.Files{[]})
412 case +h <> t:
413 do IO<Up.Answer>:
414 m : Maybe<&2, String> <- F.read(h)
415 rest : Up.Answer <- found.all(t)
416 return found.put(m, h, rest)
417
418def found.listed(ok: Bool, +text: String) -> IO(Up.Answer):
419 match ok:
420 case True{}:
421 found.all(nonempty(String.lines(text)))
422 case False{}:
423 IO.pure(Up.Answer, Up.Miss{"ez: could not list sources: " ++ String.trim(text)})
424
425# every Bend source of the project. Vendored trees and the gate's shadow live
426# under .ez, and a checkout's .git is not source, so neither is rewritten.
427def answer.sources() -> IO(Up.Answer):
428 do IO<Up.Answer>:
429 +out : String <- R.exec(["find", ".", "-name", "*.bend",
430 "-not", "-path", "*/.ez/*", "-not", "-path", "*/.git/*"])
431 found.listed(R.ok(out), R.text(out))
432
433# a checkout `ez add` walks, which could not be made or read: removed, and
434# the question answered with why
435def whole.miss(+work: String, +why: String) -> IO(Up.Answer):
436 do IO<Up.Answer>:
437 _rm : String <- R.exec(["rm", "-rf", work])
438 return Up.Miss{why}
439
440# a command of the checkout that worked goes on with it; one that failed ends
441# it
442def whole.ran(ok: Bool, +work: String, +why: String, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer):
443 match ok:
444 case True{}:
445 go(Unit{})
446 case False{}:
447 whole.miss(work, why)
448
449# one git command of the checkout, and the rest of it when it worked
450def whole.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer):
451 do IO<Up.Answer>:
452 +out : String <- R.exec(args)
453 whole.ran(R.ok(out), work, "ez: git " ++ what ++ ": " ++ String.trim(R.text(out)), go)
454
455# a file read, in front of the rest
456def whole.put(got: Maybe<&2, String>, +at: String, rest: List<&2, K.Source>) -> List<&2, K.Source>:
457 match got:
458 case None{}:
459 rest
460 case Some{text}:
461 K.Source{at, text} <> rest
462
463# every listed file of the checkout, read, with its path from the checkout's
464# top. One that cannot be read as text, such as a link to a directory, is
465# left out, as the walk would find it missing.
466def whole.read(+work: String, ps: List<&2, String>) -> IO(List<&2, K.Source>):
467 match ps:
468 case []:
469 IO.pure(List<&2, K.Source>, [])
470 case +h <> t:
471 do IO<List<&2, K.Source>>:
472 m : Maybe<&2, String> <- F.read(h)
473 rest : List<&2, K.Source> <- whole.read(work, t)
474 return whole.put(m, String.drop(h, (1n + String.length(work) : Nat)), rest)
475
476# the checkout, its `.git` removed, weighed, and every file of it read; then
477# removed. Nothing is laid: `ez add` lays the tree from its plan.
478# the files of a checkout, read, once `find` has listed them. A listing that
479# failed is refused rather than read: `find` writes what it could not reach
480# to stderr, which snap answers beside the paths (SNAP-ANS-4), and such a
481# line is no path of the package.
482def whole.listed(ok: Bool, +work: String, +nar: String, +text: String) -> IO(Up.Answer):
483 match ok:
484 case True{}:
485 do IO<Up.Answer>:
486 fs : List<&2, K.Source> <- whole.read(work, nonempty(String.lines(text)))
487 _rm : String <- R.exec(["rm", "-rf", work])
488 return Up.Cloned{nar, fs}
489 case False{}:
490 do IO<Up.Answer>:
491 _rm : String <- R.exec(["rm", "-rf", work])
492 return Up.Miss{"ez: could not list the checkout: " ++ String.trim(text)}
493
494def whole.weigh(+work: String, +rev: String) -> IO(Up.Answer):
495 do IO<Up.Answer>:
496 _dg : String <- R.exec(["rm", "-rf", work ++ "/.git"])
497 +s : Say.Spin <- Spin.hold(Say.hashing(rev))
498 +nar : String <- GX.nar(work)
499 Spin.done(s)
500 +out : String <- R.exec(["find", work, "(", "-type", "f", "-o", "-type", "l", ")"])
501 whole.listed(R.ok(out), work, nar, R.text(out))
502
503# a scratch directory made, or why it could not be
504def whole.dir(ok: Bool, +work: String, +url: String, +rev: String) -> IO(Up.Answer):
505 match ok:
506 case False{}:
507 IO.pure(Up.Answer, Up.Miss{"ez: no scratch directory to clone into: " ++ work})
508 case True{}:
509 do IO<Up.Answer>:
510 +s : Say.Spin <- Spin.hold(Say.git.fetch(url))
511 a : Up.Answer <- whole.run(work, "init", ["git", "init", "-q", work], _u =>
512 whole.run(work, "fetch", ["git", "-C", work, "fetch", "-q", "--depth", "1", url, rev], _v =>
513 whole.run(work, "checkout", ["git", "-C", work, "checkout", "-q", rev], _w =>
514 whole.weigh(work, rev))))
515 Spin.done(s)
516 return a
517
518# a checkout at a rev for `ez add`: a shallow clone into a scratch directory
519# outside the project and BEND_LIB, weighed, and every file's text read, so
520# the planner walks the package itself. The scratch directory is removed
521# whether or not the clone worked, so a refused add leaves no trace.
522def answer.whole(+url: String, +rev: String) -> IO(Up.Answer):
523 do IO<Up.Answer>:
524 +tmp : String <- R.exec(["mktemp", "-d"])
525 whole.dir(R.ok(tmp), String.trim(R.text(tmp)), url, rev)
526
527# one of the upgrade's questions, answered
528def answer.up(+lib: String, ask: Up.Ask) -> IO(Up.Answer):
529 match ask:
530 case Up.Refs{url, ref}:
531 answer.refs(url, ref)
532 case Up.Head{url}:
533 answer.head(url)
534 case Up.Tags{url}:
535 answer.tags(url)
536 case Up.Above{url, pin, tip}:
537 answer.above(lib, url, pin, tip)
538 case Up.Tree{url, rev, entry}:
539 answer.tree(lib, url, rev, entry)
540 case Up.Weigh{url, rev}:
541 answer.weigh(url, rev)
542 case Up.HubHas{hub, hash}:
543 answer.hubhas(hub, hash)
544 case Up.Ignore{}:
545 answer.ignore()
546 case Up.Sources{}:
547 answer.sources()
548 case Up.Clone{url, rev}:
549 answer.whole(url, rev)
550
551# every one of them, answered
552def answer.ups(+lib: String, asks: List<&2, Up.Ask>) -> IO(List<&2, Up.Reply>):
553 match asks:
554 case []:
555 IO.pure(List<&2, Up.Reply>, [])
556 case +h <> t:
557 do IO<List<&2, Up.Reply>>:
558 a : Up.Answer <- answer.up(lib, h)
559 rest : List<&2, Up.Reply> <- answer.ups(lib, t)
560 return Up.Reply{h, a} <> rest
561
562# every query of the lock's upgrade: git is asked the question as the planner
563# anchored it, and the answer is recorded under the question the upgrade
564# looks it up by
565def answer.queries(+lib: String, qs: List<&2, Plan.Query>) -> IO(List<&2, Up.Reply>):
566 match qs:
567 case []:
568 IO.pure(List<&2, Up.Reply>, [])
569 case Plan.Query{ask, git} <> t:
570 do IO<List<&2, Up.Reply>>:
571 a : Up.Answer <- answer.up(lib, git)
572 rest : List<&2, Up.Reply> <- answer.queries(lib, t)
573 return Up.Reply{ask, a} <> rest
574
575# the World with more answers in it
576def more(world: W.World, rs: List<&2, W.Reply>, us: List<&2, Up.Reply>) -> W.World:
577 W.World{args, ledger, listing, replies, ups} = world
578 W.World{args, ledger, listing, List.append(&2, W.Reply, replies, rs),
579 List.append(&2, Up.Reply, ups, us)}
580
581# ---------------------------------------------------------------------------
582# executing a plan
583
584# a write that did not happen stops the command
585def wrote(ok: Bool, +at: String) -> IO(Unit):
586 match ok:
587 case True{}:
588 IO.pure(Unit, Unit{})
589 case False{}:
590 IO.die(Unit, 1, "ez: " ++ at ++ " could not be written")
591
592# one file written, with the directory it goes in made first
593def write(+at: String, text: String) -> IO(Unit):
594 do IO<Unit>:
595 _mk : String <- R.exec(["mkdir", "-p", P.dir(at)])
596 ok : Bool <- F.write(at, text)
597 wrote(ok, at)
598
599# every file of a tree, in order
600def lay.all(+dir: String, fs: List<&2, W.Source>) -> IO(Unit):
601 match fs:
602 case []:
603 IO.pure(Unit, Unit{})
604 case W.Source{at, text} <> t:
605 do IO<Unit>:
606 write(dir ++ "/" ++ at, text)
607 lay.all(dir, t)
608
609# where a place is
610def place(+lib: String, pl: Plan.Place) -> String:
611 match pl:
612 case Plan.Committed{}:
613 ".ez/lib"
614 case Plan.Cache{}:
615 lib
616 case Plan.Under{dir}:
617 dir ++ "/.ez/lib"
618
619# one effect
620def exec(+lib: String, effect: Plan.Effect) -> IO(Unit):
621 match effect:
622 case Plan.Write{at, text}:
623 write(at, text)
624 case Plan.Lay{pl, hash, fs}:
625 +dir = place(lib, pl) ++ "/" ++ hash
626 do IO<Unit>:
627 _rm : String <- R.exec(["rm", "-rf", dir])
628 lay.all(dir, fs)
629 case Plan.Drop{hash}:
630 do IO<Unit>:
631 _rm : String <- R.exec(["rm", "-rf", ".ez/lib/" ++ hash])
632 return Unit{}
633 case Plan.Say{text}:
634 IO.print(text)
635 case Plan.Name{nv, hash}:
636 write(lib ++ "/names/" ++ nv, L.name.text(hash))
637
638# every effect, in order
639def exec.all(+lib: String, es: List<&2, Plan.Effect>) -> IO(Unit):
640 match es:
641 case []:
642 IO.pure(Unit, Unit{})
643 case h <> t:
644 do IO<Unit>:
645 exec(lib, h)
646 exec.all(lib, t)
647
648# a status that is 0 ends nothing, so the program runs out and exits 0; any
649# other exits with that status and the reason
650def end.at(zero: Bool, +code: U32, +why: String) -> IO(Unit):
651 match zero:
652 case True{}:
653 IO.pure(Unit, Unit{})
654 case False{}:
655 IO.die(Unit, code, why)
656
657# how the command ends: with the status the planner gave its outcome
658# (`Plan.status`, EZ-OUT-1), and the reason when it refused
659def end(+outcome: Plan.Outcome) -> IO(Unit):
660 +code = Plan.status(outcome)
661 end.at(U32.is_eq(code, 0), code, Plan.why(outcome))
662
663# a plan, executed
664def exec.plan(+lib: String, pl: Plan.Plan) -> IO(Unit):
665 Plan.Plan{es, outcome} = pl
666 do IO<Unit>:
667 exec.all(lib, es)
668 end(outcome)
669
670# ---------------------------------------------------------------------------
671# the loop
672
673# the planner's step: its questions answered and the loop gone round again,
674# or its plan run
675def loop.step(st: Plan.Step, +lib: String, back: WsUp.Back, world: W.World, go: W.World -> IO(Unit)) -> IO(Unit):
676 match st:
677 case Plan.Asking{asks, ups, names}:
678 do IO<Unit>:
679 rs : List<&2, W.Reply> <- answer.all(lib, asks)
680 us : List<&2, Up.Reply> <- answer.queries(lib, ups)
681 ns : List<&2, W.Reply> <- answer.all(lib, names)
682 go(more(world, List.append(&2, W.Reply, rs, ns), us))
683 case Plan.Run{_pl}:
684 exec.plan(lib, WsUp.plan(back, world))
685
686# the rounds the loop may take. Each round's questions come from the last
687# round's answers (a package's imports are known once git has answered for
688# it), so how many there will be is not known up front. This is a guard
689# against a planner that keeps asking, not a limit on a lock: running out of
690# it dies loudly. The walk inside each round takes its fuel from its inputs
691# (`Plan.walk.fuel`).
692def rounds() -> Nat:
693 U32.to_nat(100000)
694
695# the loop, under fuel. Every round adds at least one answer, since the
696# planner never asks what the World answers already.
697def loop(fuel: Nat, +lib: String, +back: WsUp.Back, +world: W.World) -> IO(Unit):
698 match fuel:
699 case 0n:
700 IO.die(Unit, 1, "ez: the lock kept asking questions")
701 case 1n+f:
702 loop.step(WsUp.step(back, world), lib, back, world, w => loop(f, lib, back, w))
703
704# a member's package as the lock reads it: from BEND_LIB when a tree is laid
705# there under its hash, as a git package's is (`git.seen`), and fresh from
706# the committed tree, as the joint ledger walked it, otherwise
707def settle.seen(got: Maybe<&2, String>, ask: W.Ask, answer: W.Answer, +lib: String, +hash: String) -> IO(W.Reply):
708 match got:
709 case None{}:
710 IO.pure(W.Reply, W.Reply{ask, answer})
711 case Some{text}:
712 do IO<W.Reply>:
713 a : W.Answer <- answer.lib(lib ++ "/" ++ hash, text)
714 return W.Reply{ask, a}
715
716# one member's package, so
717def settle.one(rp: W.Reply, +lib: String) -> IO(W.Reply):
718 W.Reply{+ask, answer} = rp
719 +hash = W.ask.hash(ask)
720 do IO<W.Reply>:
721 got : Maybe<&2, String> <- F.read(lib ++ "/" ++ hash ++ "/manifest")
722 settle.seen(got, ask, answer, lib, hash)
723
724# every member's package, so
725def settle(rs: List<&2, W.Reply>, +lib: String) -> IO(List<&2, W.Reply>):
726 match rs:
727 case []:
728 IO.pure(List<&2, W.Reply>, [])
729 case hd <> tl:
730 do IO<List<&2, W.Reply>>:
731 rp : W.Reply <- settle.one(hd, lib)
732 rest : List<&2, W.Reply> <- settle(tl, lib)
733 return rp <> rest
734
735# the loop, on the ledger the lock is planned from, with the workspace's
736# members already answered
737def run.planned(pd: WsRun.Planned, +lib: String, up: Bool, only: String, here: String, ls: W.Listing) -> IO(Unit):
738 WsRun.Planned{led, back, seeds} = pd
739 do IO<Unit>:
740 rs : List<&2, W.Reply> <- settle(seeds, lib)
741 loop(rounds(), lib, back, W.World{W.Args{up, only, here}, led, ls, rs, []})
742
743# `ez lock` and `ez lock --upgrade [--package only]`: the directory it runs
744# in, the ledger and the listing read, every question answered, and the plan
745# run
746def run(up: Bool, +only: String) -> IO(Unit):
747 do IO<Unit>:
748 IO.print_err(Say.resolving())
749 lib : String <- bend.lib()
750 +pwd : String <- R.exec(["pwd"])
751 own : Maybe<&2, String> <- read.ledger()
752 +ls : W.Listing <- read.listing()
753 pd : WsRun.Planned <- WsRun.ledger(own, J.files(ls))
754 run.planned(pd, lib, up, only, String.trim(R.text(pwd)), ls)