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 0x8e26a6f8c31689519e618374376ee3dd/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 import ../ws/run.bend as WsRun 31 import ../ws/up.bend as WsUp 32 import ../ws/joint.bend as J 33 34 # --------------------------------------------------------------------------- 35 # the World's first fields 36 37 # an environment variable's text; unset, "" 38 def 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 46 def lib.or(+dir: String) -> String: 47 Bool.pick(String, String.is_empty(dir), ".ez/lib", dir) 48 49 # the BEND_LIB this project pins 50 def bend.lib() -> IO(String): 51 do IO: 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 56 def 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 65 def read.ledger() -> IO(Maybe<&2, String>): 66 F.read("ez.toml") 67 68 # every listed path, read 69 def 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>: 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 80 def listed(ok: Bool, +text: String) -> IO(W.Listing): 81 match ok: 82 case True{}: 83 do IO: 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. 93 def read.listing() -> IO(W.Listing): 94 do IO: 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 103 def 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 115 def 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>: 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 126 def 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: 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 136 def answer.hub(+hub: String, +hash: String) -> IO(W.Answer): 137 do IO: 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. 146 def 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>: 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 157 def answer.lib(+dir: String, +manifest: String) -> IO(W.Answer): 158 do IO: 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 164 def clone.miss(+work: String, +why: String) -> IO(W.Answer): 165 do IO: 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 171 def 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 179 def clone.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(W.Answer)) -> IO(W.Answer): 180 do IO: 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 185 def clone.walk(+work: String, +entry: String, +nar: String) -> IO(W.Answer): 186 do IO: 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 194 def 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 202 def clone.weigh(+work: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): 203 do IO: 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. 215 def answer.clone(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): 216 +work = lib ++ "/.work-" ++ hash 217 do IO: 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 229 def 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 244 def answer.git(+lib: String, +hash: String, +url: String, +rev: String, +entry: String) -> IO(W.Answer): 245 do IO: 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 250 def 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 261 def 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 /name/` 269 def answer.name(+hub: String, +nv: String) -> IO(W.Answer): 270 do IO: 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 277 def answer.lock() -> IO(W.Answer): 278 do IO: 279 m : Maybe<&2, String> <- F.read(W.lockfile()) 280 return W.Locked{F.text_of(m)} 281 282 # one question, answered 283 def 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 293 def 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>: 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 307 def git.said(+what: String, args: List<&2, String>) -> IO(Up.Answer): 308 do IO: 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 320 def 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 325 def 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 329 def 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 333 def answer.above(+lib: String, +url: String, +pin: String, +tip: String) -> IO(Up.Answer): 334 do IO: 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 340 def tree.walk(+work: String, +at: String, +nar: String) -> IO(Up.Answer): 341 do IO: 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 350 def 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: 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. 363 def answer.tree(+lib: String, +url: String, +rev: String, +entry: String) -> IO(Up.Answer): 364 +work = lib ++ "/.work-" ++ rev 365 do IO: 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 378 def answer.weigh(+url: String, +rev: String) -> IO(Up.Answer): 379 do IO: 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 384 def answer.hubhas(+hub: String, +hash: String) -> IO(Up.Answer): 385 do IO: 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 390 def answer.ignore() -> IO(Up.Answer): 391 do IO: 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 396 def 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 408 def 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: 414 m : Maybe<&2, String> <- F.read(h) 415 rest : Up.Answer <- found.all(t) 416 return found.put(m, h, rest) 417 418 def 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. 427 def answer.sources() -> IO(Up.Answer): 428 do IO: 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 435 def whole.miss(+work: String, +why: String) -> IO(Up.Answer): 436 do IO: 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 442 def 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 450 def whole.run(+work: String, what: String, args: List<&2, String>, go: Unit -> IO(Up.Answer)) -> IO(Up.Answer): 451 do IO: 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 456 def 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. 466 def 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>: 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. 482 def whole.listed(ok: Bool, +work: String, +nar: String, +text: String) -> IO(Up.Answer): 483 match ok: 484 case True{}: 485 do IO: 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: 491 _rm : String <- R.exec(["rm", "-rf", work]) 492 return Up.Miss{"ez: could not list the checkout: " ++ String.trim(text)} 493 494 def whole.weigh(+work: String, +rev: String) -> IO(Up.Answer): 495 do IO: 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 504 def 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: 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. 522 def answer.whole(+url: String, +rev: String) -> IO(Up.Answer): 523 do IO: 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 528 def 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 552 def 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>: 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 565 def 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>: 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 576 def 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 585 def 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 593 def write(+at: String, text: String) -> IO(Unit): 594 do IO: 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 600 def 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: 606 write(dir ++ "/" ++ at, text) 607 lay.all(dir, t) 608 609 # where a place is 610 def 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 620 def 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: 627 _rm : String <- R.exec(["rm", "-rf", dir]) 628 lay.all(dir, fs) 629 case Plan.Drop{hash}: 630 do IO: 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 639 def 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: 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 650 def 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 659 def 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 664 def exec.plan(+lib: String, pl: Plan.Plan) -> IO(Unit): 665 Plan.Plan{es, outcome} = pl 666 do IO: 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 675 def 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: 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`). 692 def 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. 697 def 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 707 def 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: 713 a : W.Answer <- answer.lib(lib ++ "/" ++ hash, text) 714 return W.Reply{ask, a} 715 716 # one member's package, so 717 def 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: 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 725 def 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>: 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 737 def 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: 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 746 def run(up: Bool, +only: String) -> IO(Unit): 747 do IO: 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)