src/ws/joint.bend
ezx@1.10.0.0 · 930 lines · 0 definitions
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.
21import Base
22import ezx@1.10.0.0/src/ledger/manifest.bend as M
23import ezx@1.10.0.0/src/ledger/render.bend as Rend
24import ezx@1.10.0.0/src/pkg/path.bend as P
25import ezx@1.10.0.0/src/pkg/pkg.bend as K
26import ezx@1.10.0.0/src/lock/lock.bend as L
27import ezx@1.10.0.0/src/lock/world.bend as LW
28import ezx@1.10.0.0/src/pub/plan.bend as PP
29import ezx@1.10.0.0/src/ws/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
33type Found is Data:
34 Found{path: String, text: String}
35
36# a member's ledger, read: its directory and what it holds
37type 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
43type 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)
53def 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
63def 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
68def 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
76def 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
81def 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
89def 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
98def 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
107def 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
112def 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
120def 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
128def 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
136def 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
144def 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
152def 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
160def 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
190def 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
197def 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
205def 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
214def 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
219def 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
228def 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
237def 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
242def 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
254def 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
263def 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
272def 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
288def 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
297def pins(+ds: List<&2, M.Dep>) -> Bool:
298 pins.go(ds, ds)
299
300# the first git source pinned two ways, "" for none
301def 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
314def 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
319def 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
328def 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
340def 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
347def 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
353def 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
369def 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
376def 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
384def 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
388type 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
397type Res is Data:
398 Got{dep: M.Dep}
399 Bad{why: String}
400
401# a member's entry, as a path from the root
402def 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
406def ledger.at(+dir: List<&2, String>) -> String:
407 P.join(W.segs.text(dir), "ez.toml")
408
409# a member's `[package] name`
410def 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
416def 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
424def 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
435def 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
448def 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
458def 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
465def 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
480def 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
495def 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
500def 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
516def 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
534def 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
544type 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
549def 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
562def 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
576def 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
581def 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
593def 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
598def 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`
603def 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
612def 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
619def 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.
634def 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
644def 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
652def 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
660def 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
668def 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.
675def 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
683def 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
692def 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
701def 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
713def 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
723def 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
728def 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
737def 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
747def 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
755def 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
766def 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
774def 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
782def 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
794def 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
802def 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
811def 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
821def 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
829def 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
842def 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
855def 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
869def 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
878def 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
891def 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
899def 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
911def 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.
919def 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
923def 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)}