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