src/ledger/manifest.bend
ezx@1.7.0.0 · 461 lines · 0 definitions
1# ledger/manifest: the dependency ledger, and ez's ledger library. Bend's
2# import lines carry a bare `0x<hash>`, which says nothing about what the
3# package is or where it came from, and nothing in a repo lists them.
4# ez.toml is that list: every package the repo imports, by name, with its
5# hash and its origin.
6import Base
7import ezx@1.7.0.0/src/toml/toml.bend as T
8
9# where a package's bytes come from. A hub package needs nothing else, though
10# one added by its `<name>@<version>` keeps it, which is what its import lines
11# say and what the hub named the hash by; a git one pins a commit, and
12# remembers the tag that was asked for so an upgrade knows what to
13# re-resolve. It also carries the two facts nothing can work out again
14# without the checkout: the directory inside it the package's paths are written
15# from, and the NAR hash nix rebuilds it by. They live here, in the file that is
16# committed, because a clone gets the ledger and not the vendoring cache.
17# `vend` is the committed tree: the package is allowlisted under `.ez/lib` and
18# an upgrade that moves it lays the new hash there. Absent, it is not.
19type Source is Data:
20 Hub{named: String}
21 Git{url: String, rev: String, tag: String, root: String, nar: String, vend: Bool}
22
23# one tracked dependency
24type Dep is Data:
25 Dep{name: String, hash: String, entry: String, source: Source}
26
27# one pinned CLI. The git fields are a dependency's. It is not a package:
28# it has no `0x` name, it is not imported, and it is not put on BEND_LIB.
29# `entry` and `bin` name the file to build when the tool's own ledger should
30# not be the one.
31type Tool is Data:
32 Tool{name: String, entry: String, bin: String, source: Source}
33
34# the ledger: this package, the file `ez build` and `ez run` take, the file
35# `ez tool run`, `install` and `upgrade` build when one is named, the hub its
36# packages come from ("" for bend's own), the hub name and the version `ez
37# publish` names the package by ("" for none, which publishes it by its hash
38# alone), everything the package depends on, and every CLI it pins
39type Manifest is Data:
40 Manifest{name: String, entry: String, bin: String, hub: String,
41 publish: String, version: String,
42 deps: List<&2, Dep>, tools: List<&2, Tool>}
43
44# a manifest, or why it could not be read
45type Read is Data:
46 Bad{why: String}
47 Good{m: Manifest}
48
49# the value written for a key in a section, or "" when it is absent
50def value(pairs: List<&2, T.Kv>, key: String) -> String:
51 T.value(pairs, key)
52
53# this section's pairs when its name matches, otherwise the rest's
54def pairs.at(sect: T.Sect, rest: List<&2, T.Kv>, name: String) -> List<&2, T.Kv>:
55 T.Sect{n, ps} = sect
56 Bool.pick(List<&2, T.Kv>, String.eq(n, name), ps, rest)
57
58# the pairs of the section with this name, or none
59def pairs_of(ss: List<&2, T.Sect>, +name: String) -> List<&2, T.Kv>:
60 match ss:
61 case []:
62 []
63 case h <> t:
64 pairs.at(h, pairs_of(t, name), name)
65
66# `vendor = true`, bare or quoted, is the only spelling that commits the tree
67def flag(+word: String) -> Bool:
68 String.eq(word, "true")
69
70# no `git` key means the package lives on the hub
71def source.go(hub: Bool, url: String, rev: String, tag: String, root: String, nar: String, vend: Bool) -> Source:
72 match hub:
73 case True{}:
74 Hub{""}
75 case False{}:
76 Git{url, rev, tag, root, nar, vend}
77
78# a hub source, with the `<name>@<version>` a dependency's section records
79# under `hub`, "" for none. A git source keeps no name, and neither does a
80# tool: a tool is not imported, so nothing names it.
81def source.named(named: String, src: Source) -> Source:
82 match src:
83 case Hub{_old}:
84 Hub{named}
85 case Git{url, rev, tag, root, nar, vend}:
86 Git{url, rev, tag, root, nar, vend}
87
88# a git section that names no root was written from the top of its checkout,
89# which is the root a lock already spells out as "." and the only one a ledger
90# written before this key existed can have meant
91def root.or(+dir: String) -> String:
92 Bool.pick(String, String.is_empty(dir), ".", dir)
93
94# where a dependency's section says its bytes come from
95def source_of(+pairs: List<&2, T.Kv>) -> Source:
96 source.go(String.is_empty(value(pairs, "git")), value(pairs, "git"),
97 value(pairs, "rev"), value(pairs, "tag"), root.or(value(pairs, "root")),
98 value(pairs, "narHash"), flag(value(pairs, "vendor")))
99
100# a `[deps.<name>]` section as a dependency. A hub dependency added by its
101# `<name>@<version>` records it as `hub`, beside the hash it named when it
102# was added, as a git dependency records its tag beside its rev.
103def dep_of(+full: String, +pairs: List<&2, T.Kv>) -> Dep:
104 Dep{String.drop(full, 5n), value(pairs, "hash"), value(pairs, "entry"),
105 source.named(value(pairs, "hub"), source_of(pairs))}
106
107# a dependency's own name, for finding or replacing it
108def dep.name(dependency: Dep) -> String:
109 Dep{n, _h, _e, _s} = dependency
110 n
111
112# a dependency's hash, which is the `0x` name its import line carries
113def dep.hash(dependency: Dep) -> String:
114 Dep{_n, h, _e, _s} = dependency
115 h
116
117# the file inside the package a dependency was added by, which is the file its
118# hash was computed from
119def dep.entry(dependency: Dep) -> String:
120 Dep{_n, _h, e, _s} = dependency
121 e
122
123# a section joins the dependencies only when it is a `[deps.<name>]`
124def deps.put(is_dep: Bool, sect: T.Sect, rest: List<&2, Dep>) -> List<&2, Dep>:
125 match is_dep:
126 case True{}:
127 T.Sect{n, ps} = sect
128 dep_of(n, ps) <> rest
129 case False{}:
130 rest
131
132# a section's name, for deciding what it is
133def name_of(sect: T.Sect) -> String:
134 T.Sect{n, _ps} = sect
135 n
136
137# every `[deps.<name>]` section, as dependencies, in the order written
138def deps(ss: List<&2, T.Sect>) -> List<&2, Dep>:
139 match ss:
140 case +h <> t:
141 deps.put(String.starts_with(name_of(h), "deps."), h, deps(t))
142 case []:
143 []
144
145# a `[tools.<name>]` section as a pinned CLI. The prefix is `tools.`
146def tool_of(+full: String, +pairs: List<&2, T.Kv>) -> Tool:
147 Tool{String.drop(full, 6n), value(pairs, "entry"), value(pairs, "bin"),
148 source_of(pairs)}
149
150# a tool's own name
151def tool.name(pin: Tool) -> String:
152 Tool{n, _e, _b, _s} = pin
153 n
154
155# the file a pin names as its entry, or "" when the tool's ledger names it
156def tool.entry(pin: Tool) -> String:
157 Tool{_n, e, _b, _s} = pin
158 e
159
160# the file a pin names as its binary, or "" when the tool's ledger names it
161def tool.bin(pin: Tool) -> String:
162 Tool{_n, _e, b, _s} = pin
163 b
164
165# where a pinned CLI's bytes come from
166def tool.source(pin: Tool) -> Source:
167 Tool{_n, _e, _b, s} = pin
168 s
169
170# a section joins the tools only when it is a `[tools.<name>]`
171def tools.put(is_tool: Bool, sect: T.Sect, rest: List<&2, Tool>) -> List<&2, Tool>:
172 match is_tool:
173 case True{}:
174 T.Sect{n, ps} = sect
175 tool_of(n, ps) <> rest
176 case False{}:
177 rest
178
179# every `[tools.<name>]` section, as pinned CLIs, in the order written
180def tools(ss: List<&2, T.Sect>) -> List<&2, Tool>:
181 match ss:
182 case +h <> t:
183 tools.put(String.starts_with(name_of(h), "tools."), h, tools(t))
184 case []:
185 []
186
187# a dependency with no hash cannot be resolved, so it is reported by name
188def missing.at(bad: Bool, name: String, rest: String) -> String:
189 match bad:
190 case True{}:
191 name
192 case False{}:
193 rest
194
195# a `[deps.<name>]` section is missing its hash when the key is absent
196def missing.is(sect: T.Sect) -> Bool:
197 T.Sect{n, ps} = sect
198 Bool.and(String.starts_with(n, "deps."), String.is_empty(value(ps, "hash")))
199
200# the first dependency section with no hash, or "" when every one has one
201def missing(ss: List<&2, T.Sect>) -> String:
202 match ss:
203 case +h <> t:
204 missing.at(missing.is(h), name_of(h), missing(t))
205 case []:
206 ""
207
208# the first problem with the document, or "" when there is none. A syntax error
209# comes first, since a section that did not parse cannot be judged.
210def problem(+bad: String, +miss: String) -> String:
211 Bool.pick(String, String.is_empty(bad),
212 Bool.pick(String, String.is_empty(miss), "", miss ++ " has no hash"),
213 bad)
214
215# the ledger, once the document is known to be sound
216def build(+ss: List<&2, T.Sect>) -> Read:
217 Good{Manifest{value(pairs_of(ss, "package"), "name"),
218 value(pairs_of(ss, "package"), "entry"),
219 value(pairs_of(ss, "package"), "bin"),
220 value(pairs_of(ss, "package"), "hub"),
221 value(pairs_of(ss, "package"), "publish-as"),
222 value(pairs_of(ss, "package"), "version"), deps(ss), tools(ss)}}
223
224# the ledger, or the problem that stopped it
225def read.go(ok: Bool, why: String, ss: List<&2, T.Sect>) -> Read:
226 match ok:
227 case True{}:
228 build(ss)
229 case False{}:
230 Bad{why}
231
232# the sections judged, then read
233def read(+bad: String, +ss: List<&2, T.Sect>) -> Read:
234 read.go(String.is_empty(problem(bad, missing(ss))),
235 problem(bad, missing(ss)), ss)
236
237# the parsed document, taken apart
238def parse.go(toml: T.Toml) -> Read:
239 T.Toml{bad, _name, _pairs, sects} = toml
240 read(bad, sects)
241
242# the ledger written in an ez.toml
243def parse(text: String) -> Read:
244 parse.go(T.parse(text))
245
246# how a dependency reads on one line
247def show.source(src: Source) -> String:
248 match src:
249 case Hub{_named}:
250 "hub"
251 case Git{_url, _rev, _tag, _root, _nar, _vend}:
252 "git"
253
254# `<name>=<hash>@<hub|git>`
255def show.dep(dependency: Dep) -> String:
256 Dep{n, h, _entry, src} = dependency
257 n ++ "=" ++ h ++ "@" ++ show.source(src)
258
259# every dependency shown, in the order written. Base's List.map is over a
260# one-use list, and these are two-use, so the walk is written out.
261def show.deps(ds: List<&2, Dep>) -> List<&2, String>:
262 match ds:
263 case []:
264 []
265 case h <> t:
266 show.dep(h) <> show.deps(t)
267
268# the ledger on one line, for a test or a report
269def show.m(manifest: Manifest) -> String:
270 Manifest{n, e, _b, _h, _pa, _pv, ds, _ts} = manifest
271 n ++ " " ++ e ++ " " ++ String.join(show.deps(ds), " ")
272
273# the ledger, or the error, on one line
274def show(ledger: Read) -> String:
275 match ledger:
276 case Bad{why}:
277 "error: " ++ why
278 case Good{m}:
279 show.m(m)
280
281# this dependency when its name matches, otherwise whatever the rest gave
282def find.pick(hit: Bool, dependency: Dep, rest: Dep) -> Dep:
283 match hit:
284 case True{}:
285 dependency
286 case False{}:
287 rest
288
289# the dependency of this name, or a blank one
290def find.at(+dependency: Dep, rest: Dep, name: String) -> Dep:
291 Dep{n, _h, _e, _s} = dependency
292 find.pick(String.eq(n, name), dependency, rest)
293
294# the dependency of this name, or one with empty fields
295def find(ds: List<&2, Dep>, +name: String) -> Dep:
296 match ds:
297 case []:
298 Dep{"", "", "", Hub{""}}
299 case h <> t:
300 find.at(h, find(t, name), name)
301
302# the named dependency of a ledger that was read
303def dep(ledger: Read, name: String) -> Dep:
304 match ledger:
305 case Bad{_why}:
306 Dep{"", "", "", Hub{""}}
307 case Good{m}:
308 Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = m
309 find(ds, name)
310
311# the commit a dependency is pinned to, or "" when it is a hub package
312def source.rev(src: Source) -> String:
313 match src:
314 case Hub{_named}:
315 ""
316 case Git{_url, rev, _tag, _root, _nar, _vend}:
317 rev
318
319# the tag a dependency was asked for, or "" when it was pinned by commit
320def source.tag(src: Source) -> String:
321 match src:
322 case Hub{_named}:
323 ""
324 case Git{_url, _rev, tag, _root, _nar, _vend}:
325 tag
326
327# the repo a dependency was vendored from, or "" when it is a hub package
328def source.url(src: Source) -> String:
329 match src:
330 case Hub{_named}:
331 ""
332 case Git{url, _rev, _tag, _root, _nar, _vend}:
333 url
334
335# the directory inside the checkout a dependency's paths are written from
336def source.root(src: Source) -> String:
337 match src:
338 case Hub{_named}:
339 ""
340 case Git{_url, _rev, _tag, root, _nar, _vend}:
341 root
342
343# the NAR hash nix rebuilds a dependency's checkout by
344def source.nar(src: Source) -> String:
345 match src:
346 case Hub{_named}:
347 ""
348 case Git{_url, _rev, _tag, _root, nar, _vend}:
349 nar
350
351# the `<name>@<version>` a hub dependency was added by, or "" for one added
352# by its hash, and for a git one
353def source.nv(src: Source) -> String:
354 match src:
355 case Hub{named}:
356 named
357 case Git{_url, _rev, _tag, _root, _nar, _vend}:
358 ""
359
360# whether a dependency names a repo rather than the hub
361def source.is_git(src: Source) -> Bool:
362 match src:
363 case Hub{_named}:
364 False{}
365 case Git{_url, _rev, _tag, _root, _nar, _vend}:
366 True{}
367
368# whether an upgrade that moves this dependency also commits the new tree
369def source.vendor(src: Source) -> Bool:
370 match src:
371 case Hub{_named}:
372 False{}
373 case Git{_url, _rev, _tag, _root, _nar, vend}:
374 vend
375
376# the source of a dependency that was found
377def source.dep(dependency: Dep) -> Source:
378 Dep{_n, _h, _e, s} = dependency
379 s
380
381# the commit the named dependency is pinned to
382def rev_of(ledger: Read, name: String) -> String:
383 source.rev(source.dep(dep(ledger, name)))
384
385# the tag the named dependency was asked for
386def tag_of(ledger: Read, name: String) -> String:
387 source.tag(source.dep(dep(ledger, name)))
388
389# the hub a ledger's packages come from: the `hub` its `[package]` table
390# names, or bend's own when it names none. It is read from the committed
391# ledger and not from the environment, so two checkouts of one commit lock
392# against the same hub whatever their shells say.
393def hub.or(+url: String) -> String:
394 Bool.pick(String, String.is_empty(url), "https://hub.bend-lang.com", url)
395
396# the hub of a ledger that was read. One that would not parse names none, and
397# so names bend's.
398def hub_of(ledger: Read) -> String:
399 match ledger:
400 case Bad{_why}:
401 hub.or("")
402 case Good{m}:
403 Manifest{_n, _e, _b, h, pa, pv, _ds, _ts} = m
404 hub.or(h)
405
406# the pinned CLIs of a ledger that was read; one that would not parse has none
407def tools_of(ledger: Read) -> List<&2, Tool>:
408 match ledger:
409 case Bad{_why}:
410 []
411 case Good{m}:
412 Manifest{_n, _e, _b, _h, _pa, _pv, _ds, ts} = m
413 ts
414
415# this tool when its name matches, otherwise whatever the rest gave
416def tool.find.pick(hit: Bool, pin: Tool, rest: Tool) -> Tool:
417 match hit:
418 case True{}:
419 pin
420 case False{}:
421 rest
422
423# the tool of this name, or a blank one
424def tool.find.at(+pin: Tool, rest: Tool, +name: String) -> Tool:
425 tool.find.pick(String.eq(tool.name(pin), name), pin, rest)
426
427# the tool of this name, or one with empty fields
428def tool.find(ts: List<&2, Tool>, +name: String) -> Tool:
429 match ts:
430 case []:
431 Tool{"", "", "", Hub{""}}
432 case h <> t:
433 tool.find.at(h, tool.find(t, name), name)
434
435# the rest of the scan only when this one was not it
436def tool.step(hit: Bool, rest: Unit -> Bool) -> Bool:
437 match hit:
438 case True{}:
439 True{}
440 case False{}:
441 rest(Unit{})
442
443# whether the ledger pins a CLI of this name
444def tool.has(ts: List<&2, Tool>, +name: String) -> Bool:
445 match ts:
446 case []:
447 False{}
448 case +h <> t:
449 tool.step(String.eq(tool.name(h), name), _u => tool.has(t, name))
450
451# an empty `--package` is every tool; a name is that one
452def tool.known.go(all: Bool, ts: List<&2, Tool>, +name: String) -> Bool:
453 match all:
454 case True{}:
455 True{}
456 case False{}:
457 tool.has(ts, name)
458
459# whether `--package` names a pinned CLI, or names nothing and so names all
460def tool.known(+only: String, ts: List<&2, Tool>) -> Bool:
461 tool.known.go(String.is_empty(only), ts, only)