1 # ledger/manifest: the dependency ledger, and ez's ledger library. Bend's 2 # import lines carry a bare `0x`, 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. 6 import Base 7 import ../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 `@` 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. 19 type 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 24 type 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. 31 type 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 39 type 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 45 type 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 50 def 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 54 def 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 59 def 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 67 def flag(+word: String) -> Bool: 68 String.eq(word, "true") 69 70 # no `git` key means the package lives on the hub 71 def 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 `@` 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. 81 def 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 91 def 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 95 def 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.]` section as a dependency. A hub dependency added by its 101 # `@` records it as `hub`, beside the hash it named when it 102 # was added, as a git dependency records its tag beside its rev. 103 def 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 108 def 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 113 def 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 119 def 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.]` 124 def 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 133 def name_of(sect: T.Sect) -> String: 134 T.Sect{n, _ps} = sect 135 n 136 137 # every `[deps.]` section, as dependencies, in the order written 138 def 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.]` section as a pinned CLI. The prefix is `tools.` 146 def 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 151 def 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 156 def 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 161 def tool.bin(pin: Tool) -> String: 162 Tool{_n, _e, b, _s} = pin 163 b 164 165 # where a pinned CLI's bytes come from 166 def 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.]` 171 def 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.]` section, as pinned CLIs, in the order written 180 def 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 188 def 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.]` section is missing its hash when the key is absent 196 def 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 201 def 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. 210 def 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 216 def 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 225 def 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 233 def 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 238 def 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 243 def parse(text: String) -> Read: 244 parse.go(T.parse(text)) 245 246 # how a dependency reads on one line 247 def 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 # `=@` 255 def 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. 261 def 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 269 def 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 274 def 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 282 def 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 290 def 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 295 def 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 303 def 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 312 def 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 320 def 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 328 def 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 336 def 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 344 def 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 `@` a hub dependency was added by, or "" for one added 352 # by its hash, and for a git one 353 def 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 361 def 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 369 def 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 377 def source.dep(dependency: Dep) -> Source: 378 Dep{_n, _h, _e, s} = dependency 379 s 380 381 # the commit the named dependency is pinned to 382 def 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 386 def 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. 393 def 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. 398 def 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 407 def 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 416 def 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 424 def 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 428 def 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 436 def 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 444 def 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 452 def 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 460 def tool.known(+only: String, ts: List<&2, Tool>) -> Bool: 461 tool.known.go(String.is_empty(only), ts, only)