src/lock/run.bend

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