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, [], []})