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. 15 import Base 16 import 0xead4c7939c9eaca8d47da53b4496b731/main.bend as R 17 import ../io/file.bend as F 18 import ../hub/hub.bend as Web 19 import ../hub/get.bend as Net 20 import ../git/git.bend as Git 21 import ../git/exec.bend as GX 22 import ../pkg/pkg.bend as K 23 import ../pkg/path.bend as P 24 import ../share/say.bend as Say 25 import ../share/spin.bend as Spin 26 import ./lock.bend as L 27 import ./world.bend as W 28 import ./up.bend as Up 29 import ./plan.bend as Plan 30 31 # --------------------------------------------------------------------------- 32 # the World's first fields 33 34 # an environment variable's text; unset, "" 35 def 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 43 def lib.or(+dir: String) -> String: 44 Bool.pick(String, String.is_empty(dir), ".ez/lib", dir) 45 46 # the BEND_LIB this project pins 47 def bend.lib() -> IO(String): 48 do IO: 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 53 def 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 62 def read.ledger() -> IO(Maybe<&2, String>): 63 F.read("ez.toml") 64 65 # every listed path, read 66 def 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>: 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 77 def listed(ok: Bool, +text: String) -> IO(W.Listing): 78 match ok: 79 case True{}: 80 do IO: 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. 90 def read.listing() -> IO(W.Listing): 91 do IO: 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 100 def 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 112 def 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>: 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 123 def 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: 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 133 def answer.hub(+hub: String, +hash: String) -> IO(W.Answer): 134 do IO: 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. 143 def 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>: 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 154 def answer.lib(+dir: String, +manifest: String) -> IO(W.Answer): 155 do IO: 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 161 def clone.miss(+work: String, +why: String) -> IO(W.Answer): 162 do IO: 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 168 def 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 176 def clone.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(W.Answer)) -> IO(W.Answer): 177 do IO: 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 182 def clone.walk(+work: String, +entry: String, +nar: String) -> IO(W.Answer): 183 do IO: 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 191 def 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 199 def clone.weigh(+work: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): 200 do IO: 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. 212 def answer.clone(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): 213 +work = lib ++ "/.work-" ++ hash 214 do IO: 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 226 def 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 241 def answer.git(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): 242 do IO: 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 247 def 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 256 def 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 /name/` 264 def answer.name(+hub: String, +nv: String) -> IO(W.Answer): 265 do IO: 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 272 def answer.lock() -> IO(W.Answer): 273 do IO: 274 m : Maybe<&2, String> <- F.read(W.lockfile()) 275 return W.Locked{F.text_of(m)} 276 277 # one question, answered 278 def 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 288 def 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>: 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 302 def git.said(+what: String, args: List<&2, String>) -> IO(Up.Answer): 303 do IO: 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 315 def 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 320 def 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 324 def 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 328 def answer.above(+lib: String, +url: String, +pin: String, +tip: String) -> IO(Up.Answer): 329 do IO: 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 335 def tree.walk(+work: String, +at: String, +nar: String) -> IO(Up.Answer): 336 do IO: 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 345 def 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: 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. 358 def answer.tree(+lib: String, +url: String, +rev: String, +entry: String) -> IO(Up.Answer): 359 +work = lib ++ "/.work-" ++ rev 360 do IO: 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 373 def answer.weigh(+url: String, +rev: String) -> IO(Up.Answer): 374 do IO: 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 379 def answer.hubhas(+hub: String, +hash: String) -> IO(Up.Answer): 380 do IO: 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 385 def answer.ignore() -> IO(Up.Answer): 386 do IO: 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 391 def 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 403 def 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: 409 m : Maybe<&2, String> <- F.read(h) 410 rest : Up.Answer <- found.all(t) 411 return found.put(m, h, rest) 412 413 def 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. 422 def answer.sources() -> IO(Up.Answer): 423 do IO: 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 430 def whole.miss(+work: String, +why: String) -> IO(Up.Answer): 431 do IO: 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 437 def 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 445 def whole.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer): 446 do IO: 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 451 def 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. 461 def 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>: 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. 477 def whole.listed(ok: Bool, +work: String, +nar: String, +text: String) -> IO(Up.Answer): 478 match ok: 479 case True{}: 480 do IO: 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: 486 _rm : String <- R.exec(["rm", "-rf", work]) 487 return Up.Miss{"ez: could not list the checkout: " ++ String.trim(text)} 488 489 def whole.weigh(+work: String, +rev: String) -> IO(Up.Answer): 490 do IO: 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 499 def 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: 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. 517 def answer.whole(+url: String, +rev: String) -> IO(Up.Answer): 518 do IO: 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 523 def 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 547 def 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>: 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 560 def 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>: 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 571 def 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 580 def 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 588 def write(+at: String, text: String) -> IO(Unit): 589 do IO: 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 595 def 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: 601 write(dir ++ "/" ++ at, text) 602 lay.all(dir, t) 603 604 # where a place is 605 def 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 613 def 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: 620 _rm : String <- R.exec(["rm", "-rf", dir]) 621 lay.all(dir, fs) 622 case Plan.Drop{hash}: 623 do IO: 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 632 def 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: 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 643 def 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 652 def 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 657 def exec.plan(+lib: String, pl: Plan.Plan) -> IO(Unit): 658 Plan.Plan{es, outcome} = pl 659 do IO: 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 668 def 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: 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`). 685 def 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. 690 def 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 700 def run(up: Bool, +only: String) -> IO(Unit): 701 do IO: 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, [], []})