1 # ws/joint: the one ledger a workspace is locked from (EZ-WS-3). `ez lock` in 2 # a workspace writes one ez.lock.toml at the root, so it reads every member's 3 # ledger; rather than teach the lock planner a second kind of World, the 4 # members' ledgers are joined into one, and the lock planner runs on a World 5 # whose ledger is that one. Every law the lock planner has is about every 6 # World, so it holds of a workspace's lock as it does of a project's. The 7 # design is docs/rfc/ez-workspaces.md. 8 # 9 # The joint ledger is the root's `[package]` table with the dependencies and 10 # tools of the root and of every member, a member's relative path sources 11 # taken from the member's directory, as the root reads them. A name is one 12 # dependency, or one tool, across the workspace: the first declaration of it 13 # is the one the joint ledger holds, and every other must agree with it. 14 # 15 # A dependency section with `workspace = true` names another member by its 16 # `[package] name` (EZ-WS-5). Before the ledgers are joined, each such section 17 # is given the member's package: its hash, the `0x` name of the walk from the 18 # member's entry over the committed files, its entry as a path from the root, 19 # and the directory the walk writes its paths from. No ledger a person keeps 20 # records any of that; only the joint ledger the lock reads does. 21 import Base 22 import ../ledger/manifest.bend as M 23 import ../ledger/render.bend as Rend 24 import ../pkg/path.bend as P 25 import ../pkg/pkg.bend as K 26 import ../lock/lock.bend as L 27 import ../lock/world.bend as LW 28 import ../pub/plan.bend as PP 29 import ./workspace.bend as W 30 31 # one ez.toml git lists under the root, other than the root's own: its path 32 # from the root, and its text 33 type Found is Data: 34 Found{path: String, text: String} 35 36 # a member's ledger, read: its directory and what it holds 37 type Member is Data: 38 Member{dir: List<&2, String>, m: M.Manifest} 39 40 # what a workspace is locked from: as a project of its own, which is not a 41 # workspace; nothing, because something about the workspace was refused; or 42 # the joint ledger 43 type Joint is Data: 44 Lone{} 45 Clash{why: String} 46 Joint{m: M.Manifest} 47 48 # --------------------------------------------------------------------------- 49 # a member's sources, as the root reads them 50 51 # a source read from a member's directory: a relative path joined to it, and 52 # a URL or an absolute path as it is (`P.anchor`, EZ-LED-8) 53 def source.at(+dir: String, src: M.Source) -> M.Source: 54 match src: 55 case M.Hub{nv}: 56 M.Hub{nv} 57 case M.Git{url, rev, tag, root, nar, vend}: 58 M.Git{P.anchor(dir, url), rev, tag, root, nar, vend} 59 case M.Member{path, named}: 60 M.Member{path, named} 61 62 # a dependency read from a member's directory 63 def dep.at(+dir: String, dependency: M.Dep) -> M.Dep: 64 M.Dep{nn, hh, ee, ss} = dependency 65 M.Dep{nn, hh, ee, source.at(dir, ss)} 66 67 # every dependency read from a member's directory 68 def deps.at(+dir: String, ds: List<&2, M.Dep>) -> List<&2, M.Dep>: 69 match ds: 70 case []: 71 [] 72 case hd <> tl: 73 dep.at(dir, hd) <> deps.at(dir, tl) 74 75 # a tool read from a member's directory 76 def tool.at(+dir: String, pin: M.Tool) -> M.Tool: 77 M.Tool{nn, ee, bb, ss} = pin 78 M.Tool{nn, ee, bb, source.at(dir, ss)} 79 80 # every tool read from a member's directory 81 def tools.at(+dir: String, ts: List<&2, M.Tool>) -> List<&2, M.Tool>: 82 match ts: 83 case []: 84 [] 85 case hd <> tl: 86 tool.at(dir, hd) <> tools.at(dir, tl) 87 88 # every member's dependencies, in member order, each as the root reads it 89 def members.deps(ms: List<&2, Member>) -> List<&2, M.Dep>: 90 match ms: 91 case []: 92 [] 93 case Member{ds, mm} <> tl: 94 M.Manifest{_n, _e, _b, _h, _pa, _pv, xs, _ts} = mm 95 List.append(&2, M.Dep, deps.at(W.segs.text(ds), xs), members.deps(tl)) 96 97 # every member's tools, in member order, each as the root reads it 98 def members.tools(ms: List<&2, Member>) -> List<&2, M.Tool>: 99 match ms: 100 case []: 101 [] 102 case Member{ds, mm} <> tl: 103 M.Manifest{_n, _e, _b, _h, _pa, _pv, _xs, ts} = mm 104 List.append(&2, M.Tool, tools.at(W.segs.text(ds), ts), members.tools(tl)) 105 106 # the root's dependencies, then every member's 107 def all.deps(root: M.Manifest, ms: List<&2, Member>) -> List<&2, M.Dep>: 108 M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = root 109 List.append(&2, M.Dep, ds, members.deps(ms)) 110 111 # the root's tools, then every member's 112 def all.tools(root: M.Manifest, ms: List<&2, Member>) -> List<&2, M.Tool>: 113 M.Manifest{_n, _e, _b, _h, _pa, _pv, _ds, ts} = root 114 List.append(&2, M.Tool, ts, members.tools(ms)) 115 116 # --------------------------------------------------------------------------- 117 # one declaration per name 118 119 # every dependency, each name once, at its first declaration 120 def firsts(ds: List<&2, M.Dep>) -> List<&2, M.Dep>: 121 match ds: 122 case []: 123 [] 124 case +hd <> tl: 125 hd <> Rend.without(firsts(tl), M.dep.name(hd)) 126 127 # a tool kept or dropped 128 def tools.without.put(hit: Bool, pin: M.Tool, rest: List<&2, M.Tool>) -> List<&2, M.Tool>: 129 match hit: 130 case True{}: 131 rest 132 case False{}: 133 pin <> rest 134 135 # every tool but the one named 136 def tools.without(ts: List<&2, M.Tool>, +name: String) -> List<&2, M.Tool>: 137 match ts: 138 case []: 139 [] 140 case +hd <> tl: 141 tools.without.put(String.eq(M.tool.name(hd), name), hd, tools.without(tl, name)) 142 143 # every tool, each name once, at its first declaration 144 def tools.firsts(ts: List<&2, M.Tool>) -> List<&2, M.Tool>: 145 match ts: 146 case []: 147 [] 148 case +hd <> tl: 149 hd <> tools.without(tools.firsts(tl), M.tool.name(hd)) 150 151 # whether two bits are one 152 def bit.eq(aa: Bool, bb: Bool) -> Bool: 153 match aa: 154 case True{}: 155 bb 156 case False{}: 157 Bool.not(bb) 158 159 # whether two sources are one 160 def source.eq(aa: M.Source, bb: M.Source) -> Bool: 161 match aa: 162 case M.Hub{an}: 163 match bb: 164 case M.Hub{bn}: 165 String.eq(an, bn) 166 case M.Git{_u, _r, _t, _o, _n, _v}: 167 False{} 168 case M.Member{_p, _m}: 169 False{} 170 case M.Git{au, ar, at, ao, an, av}: 171 match bb: 172 case M.Hub{_bn}: 173 False{} 174 case M.Git{bu, br, bt, bo, bn, bv}: 175 Bool.and(Bool.and(Bool.and(String.eq(au, bu), String.eq(ar, br)), 176 Bool.and(String.eq(at, bt), String.eq(ao, bo))), 177 Bool.and(String.eq(an, bn), bit.eq(av, bv))) 178 case M.Member{_p, _m}: 179 False{} 180 case M.Member{ap, am}: 181 match bb: 182 case M.Hub{_bn}: 183 False{} 184 case M.Git{_u, _r, _t, _o, _n, _v}: 185 False{} 186 case M.Member{bp, bm}: 187 Bool.and(String.eq(ap, bp), String.eq(am, bm)) 188 189 # whether two dependencies are one 190 def dep.eq(aa: M.Dep, bb: M.Dep) -> Bool: 191 M.Dep{an, ah, ae, asrc} = aa 192 M.Dep{bn, bh, be, bsrc} = bb 193 Bool.and(Bool.and(String.eq(an, bn), String.eq(ah, bh)), 194 Bool.and(String.eq(ae, be), source.eq(asrc, bsrc))) 195 196 # whether two tools are one 197 def tool.eq(aa: M.Tool, bb: M.Tool) -> Bool: 198 M.Tool{an, ae, ab, asrc} = aa 199 M.Tool{bn, be, bb2, bsrc} = bb 200 Bool.and(Bool.and(String.eq(an, bn), String.eq(ae, be)), 201 Bool.and(String.eq(ab, bb2), source.eq(asrc, bsrc))) 202 203 # whether every dependency of a list is the first declaration of its name in 204 # the whole list 205 def agree.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> Bool: 206 match ds: 207 case []: 208 True{} 209 case +hd <> tl: 210 rest = agree.go(tl, all) 211 Bool.and(dep.eq(hd, M.find(all, M.dep.name(hd))), rest) 212 213 # whether every declaration of a dependency agrees with the first 214 def agree(+ds: List<&2, M.Dep>) -> Bool: 215 agree.go(ds, ds) 216 217 # the first dependency that does not agree with the first declaration of its 218 # name, "" for none 219 def clash.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> String: 220 match ds: 221 case []: 222 "" 223 case +hd <> tl: 224 rest = clash.go(tl, all) 225 Bool.pick(String, dep.eq(hd, M.find(all, M.dep.name(hd))), rest, M.dep.name(hd)) 226 227 # whether every declaration of a tool agrees with the first 228 def tools.agree.go(ts: List<&2, M.Tool>, +all: List<&2, M.Tool>) -> Bool: 229 match ts: 230 case []: 231 True{} 232 case +hd <> tl: 233 rest = tools.agree.go(tl, all) 234 Bool.and(tool.eq(hd, M.tool.find(all, M.tool.name(hd))), rest) 235 236 # whether every declaration of a tool agrees with the first 237 def tools.agree(+ts: List<&2, M.Tool>) -> Bool: 238 tools.agree.go(ts, ts) 239 240 # the first tool that does not agree with the first declaration of its name, 241 # "" for none 242 def tools.clash.go(ts: List<&2, M.Tool>, +all: List<&2, M.Tool>) -> String: 243 match ts: 244 case []: 245 "" 246 case +hd <> tl: 247 rest = tools.clash.go(tl, all) 248 Bool.pick(String, tool.eq(hd, M.tool.find(all, M.tool.name(hd))), rest, M.tool.name(hd)) 249 250 # --------------------------------------------------------------------------- 251 # one pin per git source (EZ-WS-4) 252 253 # the pin a source is recorded at, once it is known whether it is the url 254 def pin.at(hit: Bool, src: M.Source, rest: M.Source) -> M.Source: 255 match hit: 256 case True{}: 257 src 258 case False{}: 259 rest 260 261 # the first git source among the dependencies with this url, as the root 262 # reads it, or a hub source when there is none 263 def pin.of(ds: List<&2, M.Dep>, +url: String) -> M.Source: 264 match ds: 265 case []: 266 M.Hub{""} 267 case M.Dep{_n, _h, _e, +src} <> tl: 268 rest = pin.of(tl, url) 269 pin.at(String.eq(M.source.url(src), url), src, rest) 270 271 # whether two git sources are pinned alike: one commit and one tag 272 def pin.same(aa: M.Source, bb: M.Source) -> Bool: 273 match aa: 274 case M.Hub{_an}: 275 True{} 276 case M.Git{_au, ar, at, _ao, _an, _av}: 277 match bb: 278 case M.Hub{_bn}: 279 True{} 280 case M.Git{_bu, br, bt, _bo, _bn, _bv}: 281 Bool.and(String.eq(ar, br), String.eq(at, bt)) 282 case M.Member{_bp, _bm}: 283 True{} 284 case M.Member{_ap, _am}: 285 True{} 286 287 # whether every git dependency is pinned as the first declaration of its url 288 def pins.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> Bool: 289 match ds: 290 case []: 291 True{} 292 case M.Dep{_n, _h, _e, +src} <> tl: 293 rest = pins.go(tl, all) 294 Bool.and(pin.same(src, pin.of(all, M.source.url(src))), rest) 295 296 # whether every git source has one pin across the workspace 297 def pins(+ds: List<&2, M.Dep>) -> Bool: 298 pins.go(ds, ds) 299 300 # the first git source pinned two ways, "" for none 301 def pins.clash.go(ds: List<&2, M.Dep>, +all: List<&2, M.Dep>) -> String: 302 match ds: 303 case []: 304 "" 305 case M.Dep{_n, _h, _e, +src} <> tl: 306 rest = pins.clash.go(tl, all) 307 Bool.pick(String, pin.same(src, pin.of(all, M.source.url(src))), rest, M.source.url(src)) 308 309 # --------------------------------------------------------------------------- 310 # the hub 311 312 # whether a member's ledger names the root's hub: it names none, which is the 313 # root's, or the same one 314 def hub.one(+hub: String, mm: M.Manifest) -> Bool: 315 M.Manifest{_n, _e, _b, +hh, _pa, _pv, _ds, _ts} = mm 316 Bool.or(String.is_empty(hh), String.eq(hh, hub)) 317 318 # whether every member names the root's hub 319 def hubs(+hub: String, ms: List<&2, Member>) -> Bool: 320 match ms: 321 case []: 322 True{} 323 case Member{_ds, mm} <> tl: 324 rest = hubs(hub, tl) 325 Bool.and(hub.one(hub, mm), rest) 326 327 # the first member naming another hub, "" for none 328 def hubs.other(+hub: String, ms: List<&2, Member>) -> String: 329 match ms: 330 case []: 331 "" 332 case Member{+ds, mm} <> tl: 333 rest = hubs.other(hub, tl) 334 Bool.pick(String, hub.one(hub, mm), rest, W.segs.text(ds)) 335 336 # --------------------------------------------------------------------------- 337 # the joint ledger 338 339 # the root's hub 340 def root.hub(root: M.Manifest) -> String: 341 M.Manifest{_n, _e, _b, hh, _pa, _pv, _ds, _ts} = root 342 hh 343 344 # whether the members can be joined: every one names the root's hub, every 345 # declaration of a dependency and of a tool agrees with the first, and every 346 # git source is pinned at one commit and one tag 347 def ok(+root: M.Manifest, +ms: List<&2, Member>) -> Bool: 348 Bool.and(hubs(root.hub(root), ms), 349 Bool.and(Bool.and(agree(all.deps(root, ms)), pins(all.deps(root, ms))), 350 tools.agree(all.tools(root, ms)))) 351 352 # why the members cannot be joined 353 def why(+root: M.Manifest, +ms: List<&2, Member>) -> String: 354 +other = hubs.other(root.hub(root), ms) 355 +dep = clash.go(all.deps(root, ms), all.deps(root, ms)) 356 +pin = pins.clash.go(all.deps(root, ms), all.deps(root, ms)) 357 +tool = tools.clash.go(all.tools(root, ms), all.tools(root, ms)) 358 Bool.pick(String, String.is_empty(other), 359 Bool.pick(String, String.is_empty(dep), 360 Bool.pick(String, String.is_empty(pin), 361 "ez: workspace members pin the tool " ++ tool ++ " differently", 362 "ez: workspace members pin " ++ pin ++ " at different commits; " ++ 363 "move it for all of them with ez lock --upgrade --package"), 364 "ez: workspace members depend on " ++ dep ++ " differently"), 365 "ez: workspace member " ++ other ++ " names another hub than the root's") 366 367 # the joint ledger: the root's package, and every dependency and tool, each 368 # name at its first declaration 369 def joined(root: M.Manifest, +ms: List<&2, Member>) -> M.Manifest: 370 M.Manifest{nn, ee, bb, hh, pa, pv, ds, ts} = root 371 M.Manifest{nn, ee, bb, hh, pa, pv, 372 firsts(List.append(&2, M.Dep, ds, members.deps(ms))), 373 tools.firsts(List.append(&2, M.Tool, ts, members.tools(ms)))} 374 375 # the members joined, once it is known whether they can be 376 def combine.go(fine: Bool, +root: M.Manifest, +ms: List<&2, Member>) -> Joint: 377 match fine: 378 case True{}: 379 Joint{joined(root, ms)} 380 case False{}: 381 Clash{why(root, ms)} 382 383 # the root's ledger and every member's, joined, or why they cannot be 384 def combine(+root: M.Manifest, +ms: List<&2, Member>) -> Joint: 385 combine.go(ok(root, ms), root, ms) 386 387 # the members' ledgers read, or why one could not be 388 type Read is Data: 389 Unread{why: String} 390 Read{ms: List<&2, Member>} 391 392 # --------------------------------------------------------------------------- 393 # a member as a dependency (EZ-WS-5) 394 395 # what a member's section resolves to: the dependency on the member's 396 # package, or why there is none 397 type Res is Data: 398 Got{dep: M.Dep} 399 Bad{why: String} 400 401 # a member's entry, as a path from the root 402 def entry.at(+dir: List<&2, String>, +entry: String) -> String: 403 P.join(W.segs.text(dir), entry) 404 405 # the ledger a member's section is written in, as a path from the root 406 def ledger.at(+dir: List<&2, String>) -> String: 407 P.join(W.segs.text(dir), "ez.toml") 408 409 # a member's `[package] name` 410 def member.name(mb: Member) -> String: 411 Member{_dir, mm} = mb 412 M.Manifest{nn, _e, _b, _h, _pa, _pv, _ds, _ts} = mm 413 nn 414 415 # the member found, once it is known whether the head is it 416 def named.at(hit: Bool, hd: Member, rest: Maybe<&2, Member>) -> Maybe<&2, Member>: 417 match hit: 418 case True{}: 419 Some{hd} 420 case False{}: 421 rest 422 423 # the first member whose `[package] name` is this one, or none 424 def named(ms: List<&2, Member>, +name: String) -> Maybe<&2, Member>: 425 match ms: 426 case []: 427 None{} 428 case +hd <> tl: 429 rest = named(tl, name) 430 named.at(String.eq(member.name(hd), name), hd, rest) 431 432 # a member's package, walked, as the dependency on it: the walk's hash, the 433 # entry it walked from, the directory it writes its paths from, and the name 434 # it is imported by, "" when it is imported by its hash 435 def walked(got: K.Walked, +name: String, +entry: String, +nv: String) -> Res: 436 match got: 437 case K.Walked{hash, root, _fs, _sums}: 438 Got{M.Dep{name, hash, entry, M.Member{root, nv}}} 439 case K.Asks{at}: 440 Bad{"ez: workspace member " ++ name ++ " imports " ++ at ++ ", which git does not track"} 441 case K.Refused{why}: 442 Bad{why} 443 444 # a member's package, by the name `ez publish` would publish it under 445 # (EZ-WS-6): imported by that name when it has one, by its hash when its 446 # ledger gives neither `publish-as` nor `version`, and refused when it gives 447 # a name publish refuses 448 def by.name(named: PP.Named, got: K.Walked, +name: String, +entry: String) -> Res: 449 match named: 450 case PP.ByHash{}: 451 walked(got, name, entry, "") 452 case PP.ByName{nv}: 453 walked(got, name, entry, nv) 454 case PP.Unnamed{why}: 455 Bad{why ++ "; workspace member " ++ name ++ " is imported by the name ez publish would give it"} 456 457 # a member's package, as the dependency on it 458 def member.dep(mm: M.Manifest, +dir: List<&2, String>, +name: String, +files: List<&2, K.Source>) -> Res: 459 M.Manifest{_n, ee, _b, _h, pa, pv, _ds, _ts} = mm 460 +entry = entry.at(dir, ee) 461 by.name(PP.naming(pa, pv), K.of(entry, files), name, entry) 462 463 # the member a section names, once it is known whether it is the member 464 # that names it 465 def target.at( 466 own: Bool, 467 +name: String, 468 mm: M.Manifest, 469 +dir: List<&2, String>, 470 +files: List<&2, K.Source>, 471 +self: List<&2, String> 472 ) -> Res: 473 match own: 474 case True{}: 475 Bad{"ez: " ++ ledger.at(self) ++ " names " ++ name ++ " as a workspace member, and that member is itself"} 476 case False{}: 477 member.dep(mm, dir, name, files) 478 479 # the member a section names, once it was looked for 480 def target( 481 found: Maybe<&2, Member>, 482 +self: List<&2, String>, 483 +name: String, 484 +files: List<&2, K.Source> 485 ) -> Res: 486 match found: 487 case None{}: 488 Bad{"ez: " ++ ledger.at(self) ++ " names " ++ name ++ " as a workspace member, and no member's " ++ 489 "[package] is named " ++ name} 490 case Some{mb}: 491 Member{+dir, mm} = mb 492 target.at(W.segs.eq(dir, self), name, mm, dir, files, self) 493 494 # whether a member's section is `workspace = true` and nothing else 495 def bare(+hash: String, +entry: String, +path: String, +nv: String) -> Bool: 496 Bool.and(Bool.and(String.is_empty(hash), String.is_empty(entry)), 497 Bool.and(String.is_empty(path), String.is_empty(nv))) 498 499 # a member's section, once it is known whether it says nothing else 500 def section( 501 plain: Bool, 502 found: Maybe<&2, Member>, 503 +self: List<&2, String>, 504 +name: String, 505 +files: List<&2, K.Source> 506 ) -> Res: 507 match plain: 508 case True{}: 509 target(found, self, name, files) 510 case False{}: 511 Bad{"ez: " ++ ledger.at(self) ++ " gives the workspace member " ++ name ++ 512 " a hash, an entry or a source; its section is `workspace = true` alone"} 513 514 # a dependency of the member at `self`, by its source: a member's section 515 # given the member's package, and any other as it is 516 def dep.res.src( 517 src: M.Source, 518 +name: String, 519 +hash: String, 520 +entry: String, 521 +ms: List<&2, Member>, 522 +self: List<&2, String>, 523 +files: List<&2, K.Source> 524 ) -> Res: 525 match src: 526 case M.Hub{nv}: 527 Got{M.Dep{name, hash, entry, M.Hub{nv}}} 528 case M.Git{url, rev, tag, root, nar, vend}: 529 Got{M.Dep{name, hash, entry, M.Git{url, rev, tag, root, nar, vend}}} 530 case M.Member{+path, +nv}: 531 section(bare(hash, entry, path, nv), named(ms, name), self, name, files) 532 533 # a dependency of the member at `self`, resolved against every member 534 def dep.res( 535 dependency: M.Dep, 536 +ms: List<&2, Member>, 537 +self: List<&2, String>, 538 +files: List<&2, K.Source> 539 ) -> Res: 540 M.Dep{nn, hh, ee, src} = dependency 541 dep.res.src(src, nn, hh, ee, ms, self, files) 542 543 # a member's dependencies resolved, or why one could not be 544 type Deps is Data: 545 Deps{ds: List<&2, M.Dep>} 546 Unmet{why: String} 547 548 # a dependency resolved in front of the rest, unless the rest could not be 549 def deps.cons(got: Res, rest: Deps) -> Deps: 550 match got: 551 case Bad{why}: 552 Unmet{why} 553 case Got{dd}: 554 match rest: 555 case Unmet{why}: 556 Unmet{why} 557 case Deps{ds}: 558 Deps{dd <> ds} 559 560 # every dependency of the member at `self`, resolved, the first refusal in 561 # the order written winning 562 def deps.res( 563 ds: List<&2, M.Dep>, 564 +ms: List<&2, Member>, 565 +self: List<&2, String>, 566 +files: List<&2, K.Source> 567 ) -> Deps: 568 match ds: 569 case []: 570 Deps{[]} 571 case hd <> tl: 572 rest = deps.res(tl, ms, self, files) 573 deps.cons(dep.res(hd, ms, self, files), rest) 574 575 # a member's ledger with these dependencies 576 def with.deps(mm: M.Manifest, ds: List<&2, M.Dep>) -> M.Manifest: 577 M.Manifest{nn, ee, bb, hh, pa, pv, _ds, ts} = mm 578 M.Manifest{nn, ee, bb, hh, pa, pv, ds, ts} 579 580 # a member's ledger with its dependencies resolved, in front of the rest 581 def res.put(got: Deps, +dir: List<&2, String>, mm: M.Manifest, rest: Read) -> Read: 582 match got: 583 case Unmet{why}: 584 Unread{why} 585 case Deps{ds}: 586 match rest: 587 case Unread{why}: 588 Unread{why} 589 case Read{xs}: 590 Read{Member{dir, with.deps(mm, ds)} <> xs} 591 592 # the dependencies a member's ledger records 593 def deps.of(mm: M.Manifest) -> List<&2, M.Dep>: 594 M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = mm 595 ds 596 597 # one member's ledger resolved against every member 598 def res.one(mb: Member, +ms: List<&2, Member>, +files: List<&2, K.Source>, rest: Read) -> Read: 599 Member{+dir, +mm} = mb 600 res.put(deps.res(deps.of(mm), ms, dir, files), dir, mm, rest) 601 602 # every member's ledger resolved against every member, `ms` 603 def res.all(xs: List<&2, Member>, +ms: List<&2, Member>, +files: List<&2, K.Source>) -> Read: 604 match xs: 605 case []: 606 Read{[]} 607 case hd <> tl: 608 rest = res.all(tl, ms, files) 609 res.one(hd, ms, files, rest) 610 611 # the name `ez publish` would publish a member under, or that it has none 612 def member.naming(mb: Member) -> PP.Named: 613 Member{_dir, mm} = mb 614 M.Manifest{_n, _e, _b, _h, pa, pv, _ds, _ts} = mm 615 PP.naming(pa, pv) 616 617 # a member imported by name, as a dependency of the joint ledger, in front of 618 # the rest; any other member adds none 619 def named.one(named: PP.Named, mb: Member, +files: List<&2, K.Source>, rest: Deps) -> Deps: 620 match named: 621 case PP.ByHash{}: 622 rest 623 case PP.Unnamed{_why}: 624 rest 625 case PP.ByName{_nv}: 626 Member{+dir, +mm} = mb 627 deps.cons(member.dep(mm, dir, member.name(Member{dir, mm}), files), rest) 628 629 # every member imported by name, as a dependency of the joint ledger, whether 630 # or not a section names it, so that the lock resolves its name from the 631 # ledger and never puts it to the hub or to the lock being rewritten 632 # (EZ-WS-6). A member whose name publish refuses is imported by none, and 633 # adds none. 634 def names.deps(xs: List<&2, Member>, +files: List<&2, K.Source>) -> Deps: 635 match xs: 636 case []: 637 Deps{[]} 638 case +hd <> tl: 639 rest = names.deps(tl, files) 640 named.one(member.naming(hd), hd, files, rest) 641 642 # the root's ledger and the members', resolved, joined, the members imported 643 # by name after the root's own dependencies 644 def res.joint(xs: List<&2, Member>, +ns: List<&2, M.Dep>) -> Joint: 645 match xs: 646 case []: 647 Clash{"ez: the workspace root has no ledger"} 648 case Member{_dir, +root} <> ms: 649 combine(with.deps(root, List.append(&2, M.Dep, deps.of(root), ns)), ms) 650 651 # the members imported by name, once every ledger was resolved 652 def res.named(xs: List<&2, Member>, got: Deps) -> Joint: 653 match got: 654 case Unmet{why}: 655 Clash{why} 656 case Deps{ns}: 657 res.joint(xs, ns) 658 659 # the resolved ledgers joined, or why one could not be resolved 660 def res.read(rd: Read, got: Deps) -> Joint: 661 match rd: 662 case Unread{why}: 663 Clash{why} 664 case Read{xs}: 665 res.named(xs, got) 666 667 # the root, as the member at the root's own directory, ahead of the members 668 def with.root(root: M.Manifest, ms: List<&2, Member>) -> List<&2, Member>: 669 Member{[], root} <> ms 670 671 # the root's ledger and every member's, resolved and joined, or why they 672 # cannot be. The root is the member at the root's own directory, so a member 673 # may name it when it has a `[package]`, and its sections are resolved as a 674 # member's are. 675 def resolve(+root: M.Manifest, ms: List<&2, Member>, +files: List<&2, K.Source>) -> Joint: 676 +all = with.root(root, ms) 677 res.read(res.all(all, all, files), names.deps(all, files)) 678 679 # --------------------------------------------------------------------------- 680 # the answers the lock reads a member's package from 681 682 # a file's text in a walk's files, "" when it holds none at that path 683 def text.at(fs: List<&2, K.Source>, +at: String) -> String: 684 match fs: 685 case []: 686 "" 687 case K.Source{+fa, ft} <> tl: 688 rest = text.at(tl, at) 689 Bool.pick(String, String.eq(fa, at), ft, rest) 690 691 # the texts of a walk's files, in its manifest's order 692 def texts(sums: List<&2, K.Item>, +fs: List<&2, K.Source>) -> List<&2, String>: 693 match sums: 694 case []: 695 [] 696 case K.Item{at, _sum} <> tl: 697 text.at(fs, at) <> texts(tl, fs) 698 699 # a member's package as the lock is to read it: fresh from the committed 700 # tree, as a clone arrives, so the lock lays it under BEND_LIB 701 def seed.walked(got: K.Walked, +hash: String, +path: String, +entry: String) -> List<&2, LW.Reply>: 702 match got: 703 case K.Walked{_h, _root, fs, +sums}: 704 [LW.Reply{LW.Pkg{hash, L.Member{path, entry}, ""}, 705 LW.Got{LW.Clone{""}, K.manifest_of(sums), texts(sums, fs)}}] 706 case K.Asks{_at}: 707 [] 708 case K.Refused{_why}: 709 [] 710 711 # the answer for one dependency of the joint ledger, by its source: a 712 # member's package walked again from its entry, and nothing for any other 713 def seed.src(src: M.Source, +hash: String, +entry: String, +files: List<&2, K.Source>) -> List<&2, LW.Reply>: 714 match src: 715 case M.Hub{_nv}: 716 [] 717 case M.Git{_url, _rev, _tag, _root, _nar, _vend}: 718 [] 719 case M.Member{path, _nv}: 720 seed.walked(K.of(entry, files), hash, path, entry) 721 722 # the answer for one dependency of the joint ledger 723 def seed(dependency: M.Dep, +files: List<&2, K.Source>) -> List<&2, LW.Reply>: 724 M.Dep{_n, hh, ee, src} = dependency 725 seed.src(src, hh, ee, files) 726 727 # the answers for every member the joint ledger depends on 728 def seeds.of(ds: List<&2, M.Dep>, +files: List<&2, K.Source>) -> List<&2, LW.Reply>: 729 match ds: 730 case []: 731 [] 732 case hd <> tl: 733 List.append(&2, LW.Reply, seed(hd, files), seeds.of(tl, files)) 734 735 # the answers the lock reads the members it depends on from: none for a 736 # project of its own, or for a workspace that cannot be joined 737 def seeds(jj: Joint, +files: List<&2, K.Source>) -> List<&2, LW.Reply>: 738 match jj: 739 case Lone{}: 740 [] 741 case Clash{_why}: 742 [] 743 case Joint{mm}: 744 seeds.of(deps.of(mm), files) 745 746 # the committed files a listing names, as the walk takes them 747 def files.of(ls: List<&2, LW.Source>) -> List<&2, K.Source>: 748 match ls: 749 case []: 750 [] 751 case LW.Source{at, text} <> tl: 752 K.Source{at, text} <> files.of(tl) 753 754 # the committed files git listed, or none when it could not list them 755 def files(listing: LW.Listing) -> List<&2, K.Source>: 756 match listing: 757 case LW.Listed{ls}: 758 files.of(ls) 759 case LW.Unlisted{_why}: 760 [] 761 762 # --------------------------------------------------------------------------- 763 # from the texts git lists 764 765 # a ledger kept, when it is one under a directory 766 def ledgers.put(lg: Maybe<&2, W.Ledger>, rest: List<&2, W.Ledger>) -> List<&2, W.Ledger>: 767 match lg: 768 case None{}: 769 rest 770 case Some{ll}: 771 ll <> rest 772 773 # every ledger git lists under the root, read for the member set 774 def ledgers(fs: List<&2, Found>) -> List<&2, W.Ledger>: 775 match fs: 776 case []: 777 [] 778 case Found{+pp, +tt} <> tl: 779 ledgers.put(W.ledger.of(pp, tt), ledgers(tl)) 780 781 # a member read in front of the rest, unless the rest could not be read 782 def read.cons(+dir: List<&2, String>, rd: M.Read, rest: Read) -> Read: 783 match rd: 784 case M.Bad{why}: 785 Unread{"ez: workspace member " ++ W.segs.text(dir) ++ " has an ez.toml that does not parse: " ++ why} 786 case M.Good{mm}: 787 match rest: 788 case Unread{why}: 789 Unread{why} 790 case Read{ms}: 791 Read{Member{dir, mm} <> ms} 792 793 # a ledger git listed, kept when it is a member's 794 def read.pick(hit: Bool, +dir: List<&2, String>, +text: String, rest: Read) -> Read: 795 match hit: 796 case True{}: 797 read.cons(dir, M.parse(text), rest) 798 case False{}: 799 rest 800 801 # a ledger git listed, when it is one under a directory 802 def read.one(lg: Maybe<&2, W.Ledger>, +dirs: List<&2, List<&2, String>>, +text: String, rest: Read) -> Read: 803 match lg: 804 case None{}: 805 rest 806 case Some{ll}: 807 +dir = W.ledger.dir(ll) 808 read.pick(W.has(dirs, dir), dir, text, rest) 809 810 # every member's ledger, read in the order git listed them 811 def read.all(fs: List<&2, Found>, +dirs: List<&2, List<&2, String>>) -> Read: 812 match fs: 813 case []: 814 Read{[]} 815 case Found{+pp, +tt} <> tl: 816 rest = read.all(tl, dirs) 817 read.one(W.ledger.of(pp, tt), dirs, tt, rest) 818 819 # the members joined with the root, once each was read and every member's 820 # section resolved 821 def joint.read(rd: Read, +root: M.Manifest, +files: List<&2, K.Source>) -> Joint: 822 match rd: 823 case Unread{why}: 824 Clash{why} 825 case Read{ms}: 826 resolve(root, ms, files) 827 828 # the members joined with the root, once the root's ledger was read 829 def joint.root( 830 rd: M.Read, 831 +fs: List<&2, Found>, 832 +dirs: List<&2, List<&2, String>>, 833 +files: List<&2, K.Source> 834 ) -> Joint: 835 match rd: 836 case M.Bad{why}: 837 Clash{why} 838 case M.Good{root}: 839 joint.read(read.all(fs, dirs), root, files) 840 841 # the members joined, once the member set is known 842 def joint.members( 843 ms: W.Members, 844 +text: String, 845 +fs: List<&2, Found>, 846 +files: List<&2, K.Source> 847 ) -> Joint: 848 match ms: 849 case W.Refused{why}: 850 Clash{why} 851 case W.Members{_root, dirs}: 852 joint.root(M.parse(text), fs, dirs, files) 853 854 # what the root's `[workspace]` table says, against the ledgers git lists 855 def joint.spec(rd: W.Read, +text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Joint: 856 match rd: 857 case W.Lone{}: 858 Lone{} 859 case W.Bad{why}: 860 Clash{why} 861 case W.Root{+spec}: 862 joint.members(W.members(spec, W.root.of(text), ledgers(fs)), text, fs, files) 863 864 # --------------------------------------------------------------------------- 865 # the members, resolved, for `ez doctor` (EZ-WS-9) 866 867 # the members resolved, once each was read: the root first, as the member at 868 # its own directory 869 def resolved.read(rd: Read, +root: M.Manifest, +files: List<&2, K.Source>) -> Read: 870 match rd: 871 case Unread{why}: 872 Unread{why} 873 case Read{ms}: 874 +all = with.root(root, ms) 875 res.all(all, all, files) 876 877 # the members resolved, once the root's ledger was read 878 def resolved.root( 879 rd: M.Read, 880 +fs: List<&2, Found>, 881 +dirs: List<&2, List<&2, String>>, 882 +files: List<&2, K.Source> 883 ) -> Read: 884 match rd: 885 case M.Bad{why}: 886 Unread{why} 887 case M.Good{+root}: 888 resolved.read(read.all(fs, dirs), root, files) 889 890 # the members resolved, once the member set is known 891 def resolved.members(ms: W.Members, +text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Read: 892 match ms: 893 case W.Refused{why}: 894 Unread{why} 895 case W.Members{_root, dirs}: 896 resolved.root(M.parse(text), fs, dirs, files) 897 898 # the members resolved, by what the root's `[workspace]` table says 899 def resolved.spec(rd: W.Read, +text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Read: 900 match rd: 901 case W.Lone{}: 902 Read{[]} 903 case W.Bad{why}: 904 Unread{why} 905 case W.Root{+spec}: 906 resolved.members(W.members(spec, W.root.of(text), ledgers(fs)), text, fs, files) 907 908 # every member's ledger, the root's first, with each section resolved 909 # against every member, as the joint ledger resolves them before it joins 910 # them; none for a project of its own 911 def resolved(+text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Read: 912 resolved.spec(W.spec.of(text), text, fs, files) 913 914 # what a project is locked from: its own ledger when it is no workspace root, 915 # the joint ledger of the root and every member when it is one, or why there 916 # is none. `text` is the root's ez.toml, `fs` every other ez.toml git lists 917 # under it, and `files` the committed `.bend` files a member's package is 918 # walked over. 919 def joint(+text: String, +fs: List<&2, Found>, +files: List<&2, K.Source>) -> Joint: 920 joint.spec(W.spec.of(text), text, fs, files) 921 922 # the ledger text the lock planner reads: the joint ledger, rendered 923 def joint.text(+jj: Joint, +own: Maybe<&2, String>) -> Maybe<&2, String>: 924 match jj: 925 case Lone{}: 926 own 927 case Clash{_why}: 928 own 929 case Joint{mm}: 930 Some{Rend.show(mm)}