1 # fetch/plan: `ez fetch` as a pure planner. It reads a World 2 # (fetch/world.bend) and returns either the questions it still needs 3 # answered (`step`) or a Plan: the effects to run and how the command ends 4 # (`plan`). The interpreter (fetch/run.bend) loops on `step`, answering each 5 # question by IO, until nothing is left to ask, then executes the plan. 6 # 7 # `ez fetch` fills BEND_LIB from the lock alone, re-resolving nothing. With no 8 # ez.toml, or no ez.lock.toml, it refuses before it asks anything, as 9 # `uv sync --frozen` does: a fetch never writes a lock, so it has nothing to 10 # fill BEND_LIB from. 11 # 12 # For every package the lock records it asks first for the tree already under 13 # BEND_LIB. A tree there that the lock's checks pass is kept as it is. Any 14 # other is fetched: a git package as a shallow checkout at the pinned rev, 15 # from its source anchored at the project (`P.anchor(here, url)`), and a hub 16 # package from the lock's hub. Each file is then taken from what arrived by 17 # its path, from the package's root inside the checkout for a git package, 18 # and the package is checked before anything is laid (`good`): every file's 19 # digest equals the sum the lock records for it (EZ-FETCH-1), no path leaves 20 # the package, and the files hash to the name the lock records them under 21 # (EZ-HASH-3). A git package's checkout must also weigh to the narHash the 22 # lock records (`weighed`), so the files laid come from the tree the lock 23 # pins, the tree nix rebuilds from the same pin. Only then is the tree laid 24 # under BEND_LIB, the files the lock names and their manifest last. 25 # 26 # Every name the lock records is laid too, as the file bend reads the name's 27 # hash from, `$BEND_LIB/names/` holding the hash and a newline, so bend 28 # never asks the hub for it (EZ-HUB-4). A names file that already holds the 29 # lock's hash is kept; any other is written, and one that held another hash 30 # is said to have been rewritten, since the lock wins over it as it wins over 31 # a tree that fails its checks. A name the lock records that bend would not 32 # read back, one the hub rules out or a hash that is not a `0x` name, refuses 33 # the fetch, since its file would not be the one bend reads. 34 # 35 # The plan of a fetch that succeeds lays every tree that was fetched and every 36 # names file that is not already right, and nothing else: no ledger, no lock. A fetch that refuses, for any package, 37 # has no effect at all, so a package fetched before the one that failed is 38 # not laid either, and a tree already under BEND_LIB is never removed 39 # (EZ-OUT-2). 40 # 41 # The Effect, Plan and Outcome types are the lock's (lock/plan.bend), as for 42 # `ez add`, `ez remove` and `ez init`. 43 import Base 44 import ../toml/toml.bend as T 45 import ./world.bend as F 46 import ../lock/plan.bend as P 47 import ../lock/world.bend as W 48 import ../lock/up.bend as Up 49 import ../lock/lock.bend as L 50 import ../pkg/pkg.bend as K 51 import ../pkg/path.bend as Path 52 import ../share/sha.bend as Sha 53 54 # --------------------------------------------------------------------------- 55 # the questions 56 57 # the question that fetches a package from where the lock says it comes from: 58 # a checkout of a git package at its rev, from its source as git is to read 59 # it, a relative path taken from the project; or a hub package from the hub 60 def fetch.ask(src: L.Src, +here: String, +hub: String, +hash: String) -> F.Ask: 61 match src: 62 case L.Hub{}: 63 F.Hub{W.Pkg{hash, L.Hub{}, hub}} 64 case L.Git{url, rev, _entry, _root, _nar, _tag}: 65 F.Git{hash, Up.Clone{Path.anchor(here, url), rev}} 66 67 # --------------------------------------------------------------------------- 68 # what arrived 69 70 # a package's files as they arrived, with the directory the lock's paths are 71 # written from inside them, or why nothing arrived 72 type Tree is Data: 73 Tree{root: String, files: List<&2, K.Source>} 74 Gone{why: String} 75 76 # the files a manifest names, each with the text read for it 77 def zip(fs: List<&2, K.Item>, ss: List<&2, String>) -> List<&2, K.Source>: 78 match fs ss: 79 case Nil{} Nil{}: 80 [] 81 case Nil{} Con{_s, _t}: 82 [] 83 case Con{_f, _g} Nil{}: 84 [] 85 case Con{f, g} Con{s, t}: 86 K.Source{K.file.at(f), s} <> zip(g, t) 87 88 # a manifest and the texts of the files it names, as a tree: what is under 89 # BEND_LIB, or what the hub served 90 def tree.read(answer: W.Answer) -> Tree: 91 match answer: 92 case W.Got{_how, manifest, ss}: 93 Tree{"", zip(L.manifest.files(String.lines(manifest)), ss)} 94 case W.Miss{why}: 95 Gone{why} 96 case _: 97 Gone{"ez: a tree was not answered with one"} 98 99 # a checkout at the pinned rev, whose paths are the lock's from `root` 100 def tree.said(answer: Up.Answer, +root: String) -> Tree: 101 match answer: 102 case Up.Cloned{_nar, fs}: 103 Tree{root, fs} 104 case Up.Miss{why}: 105 Gone{why} 106 case _: 107 Gone{"ez: a checkout was not answered with one"} 108 109 # what arrived for a question 110 def tree.of(answer: F.Answer, +root: String) -> Tree: 111 match answer: 112 case F.Read{a}: 113 tree.read(a) 114 case F.Said{a}: 115 tree.said(a, root) 116 case F.Held{_got}: 117 Gone{"ez: a tree was not answered with one"} 118 119 # a text looked up, "" when it is not there 120 def text.or(got: Maybe<&2, String>) -> String: 121 match got: 122 case None{}: 123 "" 124 case Some{text}: 125 text 126 127 # where a path of the lock is in a tree: under its root inside a checkout, 128 # and as it is in a tree a manifest names 129 def key(+root: String, +at: String) -> String: 130 Bool.pick(String, String.is_empty(root), at, Path.join(root, at)) 131 132 # every file the lock names, with the text the tree has at its path. A file 133 # the tree does not have is taken as empty, which does not match the lock 134 # unless the lock records an empty file. 135 def cand(fs: List<&2, K.Item>, +root: String, +tree: List<&2, K.Source>) -> List<&2, W.Source>: 136 match fs: 137 case []: 138 [] 139 case K.Item{+at, _sum} <> t: 140 W.Source{at, text.or(K.look(tree, key(root, at)))} <> cand(t, root, tree) 141 142 # --------------------------------------------------------------------------- 143 # the checks 144 145 # a file laid, weighed by its own text 146 def weigh(src: W.Source) -> K.Item: 147 W.Source{at, +text} = src 148 K.Item{at, Sha.hex(text)} 149 150 # every file laid, weighed by its own text 151 def weighs(ls: List<&2, W.Source>) -> List<&2, K.Item>: 152 match ls: 153 case []: 154 [] 155 case h <> t: 156 weigh(h) <> weighs(t) 157 158 # whether one file is the one the lock names: the same path, and a text whose 159 # digest is the sum the lock records 160 def fit(want: K.Item, got: W.Source) -> Bool: 161 K.Item{+at, sum} = want 162 W.Source{lat, +text} = got 163 Bool.and(String.eq(lat, at), String.eq(Sha.hex(text), sum)) 164 165 # whether every file is the one the lock names, in the lock's order. A list 166 # longer or shorter than the lock's is not the package. 167 def fits(fs: List<&2, K.Item>, ls: List<&2, W.Source>) -> Bool: 168 match fs ls: 169 case Nil{} Nil{}: 170 True{} 171 case Nil{} Con{_l, _m}: 172 False{} 173 case Con{_f, _g} Nil{}: 174 False{} 175 case Con{f, g} Con{l, m}: 176 +rest = fits(g, m) 177 Bool.and(fit(f, l), rest) 178 179 # whether a path stays inside the tree it is laid in: written the way the 180 # path walk writes it, so no `.` or `//` in it, not empty, not absolute and 181 # not climbing out with `..`. A lock is not trusted with where it writes. 182 def path.safe(+at: String) -> Bool: 183 Bool.and(String.eq(Path.norm(at), at), 184 Bool.not(Bool.or(K.escapes(at), String.eq(at, ".")))) 185 186 # whether every path stays inside the tree 187 def paths.safe(ls: List<&2, W.Source>) -> Bool: 188 match ls: 189 case []: 190 True{} 191 case W.Source{+at, _text} <> t: 192 +rest = paths.safe(t) 193 Bool.and(path.safe(at), rest) 194 195 # the manifest of the files laid, weighed by their own texts 196 def manifest(+ls: List<&2, W.Source>) -> String: 197 K.manifest_of(K.files_of(weighs(ls))) 198 199 # the `0x` name of the files laid, weighed by their own texts 200 def named(+ls: List<&2, W.Source>) -> String: 201 K.hash_of(K.files_of(weighs(ls))) 202 203 # whether a package may be laid: every file is the one the lock names, no 204 # path leaves the tree, and the files hash to the name the lock records them 205 # under, so the tree laid under that name is that name's 206 def good(fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> Bool: 207 +fit = fits(fs, ls) 208 +safe = paths.safe(ls) 209 Bool.and(fit, Bool.and(safe, String.eq(hash, named(ls)))) 210 211 # the first file that is not the one the lock names, or "" when none is 212 # wrong. A path is never empty, so "" is never such a path. 213 def unfit.pick(ok: Bool, +at: String, rest: String) -> String: 214 match ok: 215 case True{}: 216 rest 217 case False{}: 218 at 219 220 # the first path that does not fit, "" when every file does 221 def unfit(fs: List<&2, K.Item>, ls: List<&2, W.Source>) -> String: 222 match fs ls: 223 case Nil{} Nil{}: 224 "" 225 case Nil{} Con{l, _m}: 226 W.Source{at, _text} = l 227 at 228 case Con{f, _g} Nil{}: 229 K.file.at(f) 230 case Con{+f, g} Con{l, m}: 231 unfit.pick(fit(f, l), K.file.at(f), unfit(g, m)) 232 233 # why a package may not be laid, naming it 234 def bad.why(fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> String: 235 +at = unfit(fs, ls) 236 Bool.pick(String, String.is_empty(at), 237 Bool.pick(String, paths.safe(ls), 238 "ez: " ++ hash ++ ": its files do not hash to its name", 239 "ez: " ++ hash ++ ": a path in the lock leaves the package"), 240 "ez: " ++ hash ++ ": " ++ at ++ " does not match the lock") 241 242 # --------------------------------------------------------------------------- 243 # one package 244 245 # where one package stands: its tree under BEND_LIB still to read, its fetch 246 # still to ask, a tree under BEND_LIB that passed and is kept, the files that 247 # arrived, or why nothing arrived 248 type Part is Data: 249 AskLaid{} 250 AskFetch{} 251 Have{} 252 Got{files: List<&2, W.Source>} 253 No{why: String} 254 255 # the files that arrived for a package, or why none did 256 def part.tree(tree: Tree, fs: List<&2, K.Item>) -> Part: 257 match tree: 258 case Tree{r, files}: 259 Got{cand(fs, r, files)} 260 case Gone{why}: 261 No{why} 262 263 # a package fetched, once it is answered 264 def part.fetch.heard(got: F.Heard, fs: List<&2, K.Item>, +root: String) -> Part: 265 match got: 266 case F.Open{}: 267 AskFetch{} 268 case F.Heard{answer}: 269 part.tree(tree.of(answer, root), fs) 270 271 # a package fetched, from where the lock says it comes from 272 def part.fetch( 273 +rs: List<&2, F.Reply>, 274 +src: L.Src, 275 fs: List<&2, K.Item>, 276 +here: String, 277 +hub: String, 278 +hash: String 279 ) -> Part: 280 part.fetch.heard(F.heard(rs, fetch.ask(src, here, hub, hash)), fs, L.src.root(src)) 281 282 # a tree under BEND_LIB that passes is kept; any other is fetched 283 def part.kept( 284 ok: Bool, 285 +rs: List<&2, F.Reply>, 286 +src: L.Src, 287 fs: List<&2, K.Item>, 288 +here: String, 289 +hub: String, 290 +hash: String 291 ) -> Part: 292 match ok: 293 case True{}: 294 Have{} 295 case False{}: 296 part.fetch(rs, src, fs, here, hub, hash) 297 298 # the tree under BEND_LIB, read: checked the way a fetched one is, and 299 # fetched when it is not there 300 def part.found( 301 tree: Tree, 302 +rs: List<&2, F.Reply>, 303 +src: L.Src, 304 +fs: List<&2, K.Item>, 305 +here: String, 306 +hub: String, 307 +hash: String 308 ) -> Part: 309 match tree: 310 case Tree{_r, files}: 311 part.kept(good(fs, hash, cand(fs, "", files)), rs, src, fs, here, hub, hash) 312 case Gone{_why}: 313 part.fetch(rs, src, fs, here, hub, hash) 314 315 # the tree under BEND_LIB, once it is read 316 def part.laid( 317 got: F.Heard, 318 +rs: List<&2, F.Reply>, 319 +src: L.Src, 320 +fs: List<&2, K.Item>, 321 +here: String, 322 +hub: String, 323 +hash: String 324 ) -> Part: 325 match got: 326 case F.Open{}: 327 AskLaid{} 328 case F.Heard{answer}: 329 part.found(tree.of(answer, ""), rs, src, fs, here, hub, hash) 330 331 # where one package stands 332 def part( 333 +rs: List<&2, F.Reply>, 334 +src: L.Src, 335 +fs: List<&2, K.Item>, 336 +here: String, 337 +hub: String, 338 +hash: String 339 ) -> Part: 340 part.laid(F.heard(rs, F.Laid{hash}), rs, src, fs, here, hub, hash) 341 342 # --------------------------------------------------------------------------- 343 # the checkout around a git package 344 345 # why a checkout may not be laid for the narHash it weighed to, "" when it 346 # may: the lock records a narHash and the checkout weighed to it 347 def nar.why( 348 none: Bool, 349 same: Bool, 350 +hash: String, 351 +want: String, 352 +got: String, 353 +url: String, 354 +rev: String 355 ) -> String: 356 match none: 357 case True{}: 358 "ez: " ++ hash ++ ": the lock records no narHash for " ++ url ++ " at " ++ rev ++ 359 ", so its checkout cannot be checked; run `ez lock --upgrade` to pin it" 360 case False{}: 361 match same: 362 case True{}: 363 "" 364 case False{}: 365 "ez: " ++ hash ++ ": " ++ url ++ " at " ++ rev ++ " has narHash " ++ got ++ 366 ", and the lock records " ++ want 367 368 # why a package stands without a weighed checkout 369 def unweighed(+hash: String) -> String: 370 "ez: " ++ hash ++ ": its checkout was not weighed" 371 372 # what a checkout question was answered with, against the lock's narHash 373 def weighed.up(answer: Up.Answer, +hash: String, +nar: String, +url: String, +rev: String) -> String: 374 match answer: 375 case Up.Cloned{+got, _fs}: 376 nar.why(String.is_empty(nar), String.eq(got, nar), hash, nar, got, url, rev) 377 case _: 378 unweighed(hash) 379 380 def weighed.answer(answer: F.Answer, +hash: String, +nar: String, +url: String, +rev: String) -> String: 381 match answer: 382 case F.Said{a}: 383 weighed.up(a, hash, nar, url, rev) 384 case F.Read{_a}: 385 unweighed(hash) 386 case F.Held{_got}: 387 unweighed(hash) 388 389 def weighed.heard(got: F.Heard, +hash: String, +nar: String, +url: String, +rev: String) -> String: 390 match got: 391 case F.Heard{a}: 392 weighed.answer(a, hash, nar, url, rev) 393 case F.Open{}: 394 unweighed(hash) 395 396 def weighed.src(src: L.Src, +rs: List<&2, F.Reply>, +here: String, +hash: String) -> String: 397 match src: 398 case L.Hub{}: 399 "" 400 case L.Git{+url, +rev, _entry, _root, nar, _tag}: 401 weighed.heard(F.heard(rs, F.Git{hash, Up.Clone{Path.anchor(here, url), rev}}), 402 hash, nar, url, rev) 403 404 # why the checkout a package of the lock was fetched as may not be laid, "" 405 # when it may. A hub package has no checkout. A git package's checkout, the 406 # one its fetch question was answered with, must weigh to the narHash the 407 # lock records, so what is laid comes from the tree the lock pins and not 408 # only from files that happen to match their sums. 409 def weighed(+ss: List<&2, T.Sect>, +rs: List<&2, F.Reply>, +here: String, +hash: String) -> String: 410 weighed.src(L.pack.src(L.pack_of(ss, hash)), rs, here, hash) 411 412 def weigh.got(ok: Bool, +why: String, ls: List<&2, W.Source>) -> Part: 413 match ok: 414 case True{}: 415 Got{ls} 416 case False{}: 417 No{why} 418 419 # files that arrived stand only when the checkout they came from weighed to 420 # the lock's narHash. A tree kept from BEND_LIB was not fetched, and is 421 # checked file by file as before. 422 def weigh.part(+why: String, part: Part) -> Part: 423 match part: 424 case AskLaid{}: 425 AskLaid{} 426 case AskFetch{}: 427 AskFetch{} 428 case Have{}: 429 Have{} 430 case Got{ls}: 431 weigh.got(String.is_empty(why), why, ls) 432 case No{w}: 433 No{w} 434 435 # one package of the lock, and where it stands 436 type Pt is Data: 437 Pt{hash: String, src: L.Src, files: List<&2, K.Item>, part: Part} 438 439 # every package of the lock, and where each stands 440 def parts( 441 +ss: List<&2, T.Sect>, 442 +rs: List<&2, F.Reply>, 443 +here: String, 444 +hub: String, 445 hs: List<&2, String> 446 ) -> List<&2, Pt>: 447 match hs: 448 case []: 449 [] 450 case +h <> t: 451 +pk = L.pack_of(ss, h) 452 +src = L.pack.src(pk) 453 +fs = L.pack.files(pk) 454 Pt{h, src, fs, weigh.part(weighed(ss, rs, here, h), part(rs, src, fs, here, hub, h))} 455 <> parts(ss, rs, here, hub, t) 456 457 # --------------------------------------------------------------------------- 458 # the names 459 460 # where one name of the lock stands: its names file still to read, a file 461 # that already holds the lock's hash, a file to write, with the hash the one 462 # there held ("" when there was none), or why the name refuses the fetch 463 type Nm is Data: 464 NmAsk{nv: String} 465 NmKeep{} 466 NmPut{nv: String, hash: String, was: String} 467 NmStop{why: String} 468 469 # a names file that was there, against the lock's hash. bend trims the file 470 # before it reads the hash, and so does this. 471 def name.held(same: Bool, +nv: String, +hash: String, +was: String) -> Nm: 472 match same: 473 case True{}: 474 NmKeep{} 475 case False{}: 476 NmPut{nv, hash, was} 477 478 # what BEND_LIB holds for a name 479 def name.got(got: Maybe<&2, String>, +nv: String, +hash: String) -> Nm: 480 match got: 481 case None{}: 482 NmPut{nv, hash, ""} 483 case Some{text}: 484 +was = String.trim(text) 485 name.held(String.eq(was, hash), nv, hash, was) 486 487 # a names file, once it is read 488 def name.answer(answer: F.Answer, +nv: String, +hash: String) -> Nm: 489 match answer: 490 case F.Held{got}: 491 name.got(got, nv, hash) 492 case _: 493 NmStop{"ez: " ++ nv ++ ": its names file was not read"} 494 495 def name.heard(got: F.Heard, +nv: String, +hash: String) -> Nm: 496 match got: 497 case F.Open{}: 498 NmAsk{nv} 499 case F.Heard{a}: 500 name.answer(a, nv, hash) 501 502 # a name bend would read back is looked for under BEND_LIB; any other stops 503 # the fetch, since the file laid for it would not be the one bend reads 504 def name.valid(ok: Bool, +rs: List<&2, F.Reply>, +nv: String, +hash: String) -> Nm: 505 match ok: 506 case True{}: 507 name.heard(F.heard(rs, F.Named{nv}), nv, hash) 508 case False{}: 509 NmStop{"ez: the lock names " ++ nv ++ " as " ++ hash ++ 510 ", which bend would not read back; run ez lock"} 511 512 # where one name of the lock stands 513 def name.part(+rs: List<&2, F.Reply>, name: L.Name) -> Nm: 514 L.Name{+nv, +hash} = name 515 name.valid(L.name.valid(L.Name{nv, hash}), rs, nv, hash) 516 517 # where every name of the lock stands 518 def name.parts(+rs: List<&2, F.Reply>, ns: List<&2, L.Name>) -> List<&2, Nm>: 519 match ns: 520 case []: 521 [] 522 case h <> t: 523 name.part(rs, h) <> name.parts(rs, t) 524 525 # how a rewritten names file is said to have been rewritten 526 def name.rewrote(+nv: String, +hash: String, +was: String) -> String: 527 "ez: BEND_LIB/names/" ++ nv ++ " named " ++ was ++ ", and the lock names " ++ hash ++ 528 "; rewrote it" 529 530 # a names file written, said first when it held another hash 531 def name.put.at(none: Bool, +nv: String, +hash: String, was: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>: 532 match none: 533 case True{}: 534 P.Name{nv, hash} <> rest 535 case False{}: 536 P.Say{name.rewrote(nv, hash, was)} <> P.Name{nv, hash} <> rest 537 538 def name.put(+nv: String, +hash: String, +was: String, rest: List<&2, P.Effect>) -> List<&2, P.Effect>: 539 name.put.at(String.is_empty(was), nv, hash, was, rest) 540 541 # every names file a fetch writes, ahead of what follows 542 def names.lay(ms: List<&2, Nm>, rest: List<&2, P.Effect>) -> List<&2, P.Effect>: 543 match ms: 544 case []: 545 rest 546 case m <> t: 547 match m: 548 case NmPut{nv, hash, was}: 549 name.put(nv, hash, was, names.lay(t, rest)) 550 case _: 551 names.lay(t, rest) 552 553 # how the names end the fetch: the first that refuses, or `o`, how the 554 # packages ended it, which comes first 555 def names.outcome(ms: List<&2, Nm>) -> P.Outcome: 556 match ms: 557 case []: 558 P.Success{} 559 case m <> t: 560 match m: 561 case NmStop{why}: 562 P.Refused{why} 563 case NmAsk{nv}: 564 P.Refused{"ez: " ++ nv ++ " was never looked for under BEND_LIB"} 565 case _: 566 names.outcome(t) 567 568 # the packages' end, or, when they succeed, the names' 569 def outcome.then(first: P.Outcome, then: P.Outcome) -> P.Outcome: 570 match first: 571 case P.Success{}: 572 then 573 case P.Refused{why}: 574 P.Refused{why} 575 576 # the names still to read under BEND_LIB 577 def names.asks(ms: List<&2, Nm>) -> List<&2, F.Ask>: 578 match ms: 579 case []: 580 [] 581 case m <> t: 582 match m: 583 case NmAsk{nv}: 584 F.Named{nv} <> names.asks(t) 585 case _: 586 names.asks(t) 587 588 # every name the lock records, and where each stands 589 def name.parts.of(+ss: List<&2, T.Sect>, +world: F.World) -> List<&2, Nm>: 590 name.parts(F.replies(world), L.names(ss)) 591 592 # --------------------------------------------------------------------------- 593 # the verdict 594 595 # what one package comes to: nothing to do, a tree to lay, or a refusal 596 type Verdict is Data: 597 Keep{} 598 Put{hash: String, files: List<&2, W.Source>} 599 Stop{why: String} 600 601 # the files that arrived, laid when they pass 602 def judge.got(ok: Bool, fs: List<&2, K.Item>, +hash: String, +ls: List<&2, W.Source>) -> Verdict: 603 match ok: 604 case True{}: 605 Put{hash, ls} 606 case False{}: 607 Stop{bad.why(fs, hash, ls)} 608 609 # what one package comes to, where it stands 610 def judge(+fs: List<&2, K.Item>, +hash: String, part: Part) -> Verdict: 611 match part: 612 case AskLaid{}: 613 Stop{"ez: " ++ hash ++ " was never looked for under BEND_LIB"} 614 case AskFetch{}: 615 Stop{"ez: " ++ hash ++ " was never fetched"} 616 case Have{}: 617 Keep{} 618 case Got{+ls}: 619 judge.got(good(fs, hash, ls), fs, hash, ls) 620 case No{why}: 621 Stop{why} 622 623 # what every package comes to 624 def judged(ps: List<&2, Pt>) -> List<&2, Verdict>: 625 match ps: 626 case []: 627 [] 628 case Pt{h, _src, fs, p} <> t: 629 judge(fs, h, p) <> judged(t) 630 631 # a tree laid under BEND_LIB: the files the lock names, then their manifest, 632 # so a tree that has one is a tree that finished 633 def lay(+hash: String, +ls: List<&2, W.Source>) -> P.Effect: 634 P.Lay{P.Cache{}, hash, List.append(&2, W.Source, ls, [W.Source{"manifest", manifest(ls)}])} 635 636 # one verdict's effect, ahead of the rest 637 def effect.v(verdict: Verdict, rest: List<&2, P.Effect>) -> List<&2, P.Effect>: 638 match verdict: 639 case Keep{}: 640 rest 641 case Put{hash, ls}: 642 lay(hash, ls) <> rest 643 case Stop{_why}: 644 rest 645 646 # every verdict's effect 647 def effects.of(vs: List<&2, Verdict>) -> List<&2, P.Effect>: 648 match vs: 649 case []: 650 [] 651 case v <> t: 652 effect.v(v, effects.of(t)) 653 654 # how the command ends: the first refusal, or success 655 def outcome.of(vs: List<&2, Verdict>) -> P.Outcome: 656 match vs: 657 case []: 658 P.Success{} 659 case v <> t: 660 match v: 661 case Stop{why}: 662 P.Refused{why} 663 case _: 664 outcome.of(t) 665 666 # a plan that refuses has no effect at all; its reason is how it ends 667 def seal(es: List<&2, P.Effect>, outcome: P.Outcome) -> P.Plan: 668 match outcome: 669 case P.Success{}: 670 P.Plan{es, P.Success{}} 671 case P.Refused{why}: 672 P.Plan{[], P.Refused{why}} 673 674 # the plan once every package and every name stands somewhere: the names 675 # files, then the trees, and how the command ends. Each verdict is made once 676 # and read by both halves. 677 def parts.plan(ps: List<&2, Pt>, +ms: List<&2, Nm>) -> P.Plan: 678 +vs = judged(ps) 679 seal(names.lay(ms, effects.of(vs)), outcome.then(outcome.of(vs), names.outcome(ms))) 680 681 # --------------------------------------------------------------------------- 682 # the plan 683 684 # every package of the lock on a World that has one, and where each stands 685 def parts.of(+ss: List<&2, T.Sect>, +world: F.World) -> List<&2, Pt>: 686 parts(ss, F.replies(world), F.here(world), L.lock.hub(ss), L.hashes(ss)) 687 688 # why a fetch refuses before anything is asked: no ez.toml, or no lock 689 def need.no() -> String: 690 "ez: no " ++ F.toml() ++ " here; ez fetch fills BEND_LIB for a project's lock, " ++ 691 "and ez init makes one" 692 693 def need.lock() -> String: 694 "ez: no " ++ F.lockfile() ++ " here; ez fetch reads the lock and resolves " ++ 695 "nothing, and ez lock writes it" 696 697 # the plan once the ledger and the lock are known to be there 698 def plan.lock(got: Maybe<&2, String>, +world: F.World) -> P.Plan: 699 match got: 700 case None{}: 701 P.Plan{[], P.Refused{need.lock()}} 702 case Some{_text}: 703 +ss = F.sects(world) 704 parts.plan(parts.of(ss, world), name.parts.of(ss, world)) 705 706 # the plan: a missing ledger, then a missing lock, refused before the rest 707 def plan.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> P.Plan: 708 match led: 709 case False{}: 710 P.Plan{[], P.Refused{need.no()}} 711 case True{}: 712 plan.lock(got, world) 713 714 # `ez fetch` over a World 715 def plan(+world: F.World) -> P.Plan: 716 plan.led(F.ledger(world), F.lock(world), world) 717 718 # whether a plan ends in a refusal 719 def refuses.plan(pl: P.Plan) -> Bool: 720 P.Plan{_es, outcome} = pl 721 match outcome: 722 case P.Success{}: 723 False{} 724 case P.Refused{_why}: 725 True{} 726 727 # whether `ez fetch` refuses on a World 728 def refuses(+world: F.World) -> Bool: 729 refuses.plan(plan(world)) 730 731 # --------------------------------------------------------------------------- 732 # what is still to ask 733 734 # one package's open question, ahead of the rest 735 def ask.pt(pt: Pt, +here: String, +hub: String, rest: List<&2, F.Ask>) -> List<&2, F.Ask>: 736 Pt{+h, src, _fs, p} = pt 737 match p: 738 case AskLaid{}: 739 F.Laid{h} <> rest 740 case AskFetch{}: 741 fetch.ask(src, here, hub, h) <> rest 742 case _: 743 rest 744 745 # every open question 746 def asks.of(ps: List<&2, Pt>, +here: String, +hub: String) -> List<&2, F.Ask>: 747 match ps: 748 case []: 749 [] 750 case p <> t: 751 ask.pt(p, here, hub, asks.of(t, here, hub)) 752 753 # the open questions once the ledger and the lock are known to be there 754 def wants.lock(got: Maybe<&2, String>, +world: F.World) -> List<&2, F.Ask>: 755 match got: 756 case None{}: 757 [] 758 case Some{_text}: 759 +ss = F.sects(world) 760 List.append(&2, F.Ask, asks.of(parts.of(ss, world), F.here(world), L.lock.hub(ss)), 761 names.asks(name.parts.of(ss, world))) 762 763 def wants.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> List<&2, F.Ask>: 764 match led: 765 case False{}: 766 [] 767 case True{}: 768 wants.lock(got, world) 769 770 # the questions a World still leaves open: none when the fetch refuses before 771 # it asks, and otherwise one for each package that does not yet stand 772 # anywhere 773 def wants(+world: F.World) -> List<&2, F.Ask>: 774 wants.led(F.ledger(world), F.lock(world), world) 775 776 # the questions still open, or the plan once there are none 777 type Step is Data: 778 Asking{asks: List<&2, F.Ask>} 779 Run{plan: P.Plan} 780 781 def step.asks(asks: List<&2, F.Ask>, ps: List<&2, Pt>, ms: List<&2, Nm>) -> Step: 782 match asks: 783 case []: 784 Run{parts.plan(ps, ms)} 785 case h <> t: 786 Asking{h <> t} 787 788 def step.lock(got: Maybe<&2, String>, +world: F.World) -> Step: 789 match got: 790 case None{}: 791 Run{P.Plan{[], P.Refused{need.lock()}}} 792 case Some{_text}: 793 +ss = F.sects(world) 794 +ps = parts.of(ss, world) 795 +ms = name.parts.of(ss, world) 796 step.asks(List.append(&2, F.Ask, asks.of(ps, F.here(world), L.lock.hub(ss)), names.asks(ms)), 797 ps, ms) 798 799 def step.led(led: Bool, got: Maybe<&2, String>, +world: F.World) -> Step: 800 match led: 801 case False{}: 802 Run{P.Plan{[], P.Refused{need.no()}}} 803 case True{}: 804 step.lock(got, world) 805 806 # what the interpreter does next on a World. Every package is placed once per 807 # round, and both the questions and the plan are read from that. 808 def step(+world: F.World) -> Step: 809 step.led(F.ledger(world), F.lock(world), world) 810 811 # --------------------------------------------------------------------------- 812 # law vocabulary 813 814 # the effects of a plan 815 def effects(pl: P.Plan) -> List<&2, P.Effect>: 816 P.Plan{es, _outcome} = pl 817 es 818 819 # every file of a tree but the last, which a `Lay` writes as the manifest 820 def front(fs: List<&2, W.Source>) -> List<&2, W.Source>: 821 match fs: 822 case []: 823 [] 824 case h <> t: 825 match t: 826 case []: 827 [] 828 case h2 <> t2: 829 h <> front(h2 <> t2) 830 831 # the files the lock records under a hash 832 def locked(+ss: List<&2, T.Sect>, +hash: String) -> List<&2, K.Item>: 833 L.pack.files(L.pack_of(ss, hash)) 834 835 # whether an effect that lays a tree lays the files the lock records under 836 # its hash, each with a text whose digest is the lock's sum for it 837 def lays.one(+ss: List<&2, T.Sect>, effect: P.Effect) -> Bool: 838 match effect: 839 case P.Lay{_place, hash, fs}: 840 fits(locked(ss, hash), front(fs)) 841 case _: 842 True{} 843 844 # whether every tree a list of effects lays is the one the lock records 845 def lays.locked(+ss: List<&2, T.Sect>, es: List<&2, P.Effect>) -> Bool: 846 match es: 847 case []: 848 True{} 849 case h <> t: 850 +rest = lays.locked(ss, t) 851 Bool.and(lays.one(ss, h), rest) 852 853 # the source a checkout is asked of, "" for any other question 854 def clone.url(ask: Up.Ask) -> String: 855 match ask: 856 case Up.Clone{url, _rev}: 857 url 858 case _: 859 "" 860 861 # the package a question is about 862 def ask.hash(ask: F.Ask) -> String: 863 match ask: 864 case F.Laid{hash}: 865 hash 866 case F.Git{hash, _q}: 867 hash 868 case F.Hub{q}: 869 W.ask.hash(q) 870 case F.Named{nv}: 871 nv 872 873 # whether a git question asks for the source the lock records for its 874 # package, taken from the project: the lock's url anchored at `here` 875 def ask.anchored(+here: String, src: L.Src, ask: F.Ask) -> Bool: 876 match ask: 877 case F.Git{_hash, q}: 878 String.eq(clone.url(q), Path.anchor(here, L.src.url(src))) 879 case _: 880 True{} 881 882 # whether every question does 883 def asks.anchored(+here: String, +ss: List<&2, T.Sect>, as: List<&2, F.Ask>) -> Bool: 884 match as: 885 case []: 886 True{} 887 case +h <> t: 888 +rest = asks.anchored(here, ss, t) 889 Bool.and(ask.anchored(here, L.pack.src(L.pack_of(ss, ask.hash(h))), h), rest) 890 891 # whether an effect that lays a tree lays a package whose checkout, when it 892 # has one, weighed to the narHash the lock records 893 def weighed.lay( 894 +ss: List<&2, T.Sect>, 895 +rs: List<&2, F.Reply>, 896 +here: String, 897 effect: P.Effect 898 ) -> Bool: 899 match effect: 900 case P.Lay{_place, hash, _fs}: 901 String.is_empty(weighed(ss, rs, here, hash)) 902 case _: 903 True{} 904 905 # whether every tree a list of effects lays does 906 def lays.weighed( 907 +ss: List<&2, T.Sect>, 908 +rs: List<&2, F.Reply>, 909 +here: String, 910 es: List<&2, P.Effect> 911 ) -> Bool: 912 match es: 913 case []: 914 True{} 915 case h <> t: 916 +rest = lays.weighed(ss, rs, here, t) 917 Bool.and(weighed.lay(ss, rs, here, h), rest)