1 # ws/run: the interpreter's half of workspaces. It reads what the root search 2 # asks for (ws/root.bend) and runs the hop ws/hop.bend decides, and for `ez 3 # lock` it reads every ez.toml git lists under a workspace root so that 4 # ws/joint.bend can join them. It decides nothing: where the root is, whether 5 # to run again there, which ledgers are members and what the lock is planned 6 # from are the pure modules'. That it reads and runs faithfully is 7 # EZ-TRUST-2. It runs programs, so no law imports it. 8 import Base 9 import 0x8e26a6f8c31689519e618374376ee3dd/main.bend as Sn 10 import ../io/file.bend as F 11 import ../share/args.bend as Args 12 import ../share/pass.bend as Pass 13 import ../ledger/render.bend as Rend 14 import ./workspace.bend as W 15 import ./root.bend as R 16 import ./hop.bend as H 17 import ./joint.bend as J 18 import ./up.bend as U 19 import ./pin.bend as Pin 20 import ../ledger/manifest.bend as M 21 import ../pkg/path.bend as P 22 import ../pkg/pkg.bend as K 23 import ../lock/world.bend as LW 24 import ./doctor.bend as Doc 25 import ./relock.bend as Rel 26 27 # what the search found, and the top of the checkout it searched from 28 type Searched is Data: 29 Searched{top: String, found: R.Found} 30 31 # a directory's ez.toml, from the top of the checkout 32 def ledger.path(+top: String, ds: List<&2, String>) -> String: 33 top ++ "/" ++ W.segs.text(List.append(&2, String, ds, ["ez.toml"])) 34 35 # the ez.toml of every directory the search asks about, nearest first 36 def read.ups(+top: String, asks: List<&2, R.Ask>) -> IO(List<&2, R.Up>): 37 match asks: 38 case []: 39 IO.pure(List<&2, R.Up>, []) 40 case R.Ask{+ds, rs} <> tl: 41 do IO>: 42 tx : Maybe<&2, String> <- F.read(ledger.path(top, ds)) 43 rest : List<&2, R.Up> <- read.ups(top, tl) 44 return R.Up{ds, rs, tx} <> rest 45 46 # the search, once git said where ez runs. Outside a checkout there is no 47 # committed ledger above, so no workspace: the directory is its own project. 48 def search.at(here: Maybe<&2, List<&2, String>>, +top: String) -> IO(Searched): 49 match here: 50 case None{}: 51 IO.pure(Searched, Searched{"", R.Alone{}}) 52 case Some{hs}: 53 do IO: 54 ups : List<&2, R.Up> <- read.ups(top, R.asks(hs)) 55 return Searched{top, R.root.go(ups)} 56 57 # where ez runs, from what git printed, or nothing when git could not say 58 def here.git(ok: Bool, +text: String) -> Maybe<&2, List<&2, String>>: 59 match ok: 60 case True{}: 61 R.here.of(String.trim(text)) 62 case False{}: 63 None{} 64 65 # the search for the root, from where ez runs up to the top of its checkout 66 def search() -> IO(Searched): 67 do IO: 68 +pre : String <- Sn.exec(["git", "rev-parse", "--show-prefix"]) 69 +top : String <- Sn.exec(["git", "rev-parse", "--show-toplevel"]) 70 search.at(here.git(Bool.and(Sn.ok(pre), Sn.ok(top)), Sn.text(pre)), String.trim(Sn.text(top))) 71 72 # the program's own name, and every word after it 73 def argv() -> IO(List<&2, String>): 74 do IO>: 75 xs : List<&1, String> <- IO.args() 76 return Args.copy(xs) 77 78 # the same words run again at the root 79 def hop.go(+dir: String, +pwd: String, xs: List<&2, String>) -> IO(U32): 80 match xs: 81 case []: 82 IO.pure(U32, 1) 83 case +prog <> rest: 84 Pass.run(H.line(dir, pwd, prog, rest)) 85 86 # the hop's status, as this command's 87 def hop.end(zero: Bool, +code: U32) -> IO(Unit): 88 match zero: 89 case True{}: 90 IO.pure(Unit, Unit{}) 91 case False{}: 92 IO.exit(code) 93 94 # a run at the root, which ends as that run ended 95 def hop.run(+dir: String) -> IO(Unit): 96 do IO: 97 +pwd : String <- Sn.exec(["pwd"]) 98 xs : List<&2, String> <- argv() 99 +code : U32 <- hop.go(dir, String.trim(Sn.text(pwd)), xs) 100 hop.end(U32.is_eq(code, 0), code) 101 102 # a command acted on as its hop says 103 def hop.at(hop: H.Hop, go: Unit -> IO(Unit)) -> IO(Unit): 104 match hop: 105 case H.Here{}: 106 go(Unit{}) 107 case H.Stop{why}: 108 IO.die(Unit, 1, why) 109 case H.Hop{dir}: 110 hop.run(dir) 111 112 # what the search found, acted on 113 def at.searched(st: Searched, go: Unit -> IO(Unit)) -> IO(Unit): 114 Searched{+top, found} = st 115 hop.at(H.hop(top, found), go) 116 117 # a whole-workspace command, `ez lock`, `ez fetch` or `ez doctor`: run where 118 # it is when that is its project's root, and again at the root when it is run 119 # in a member 120 def at.root(go: Unit -> IO(Unit)) -> IO(Unit): 121 do IO: 122 st : Searched <- search() 123 at.searched(st, go) 124 125 # --------------------------------------------------------------------------- 126 # the ledger `ez lock` is planned from 127 128 # every listed ez.toml, read 129 def read.found(ps: List<&2, String>) -> IO(List<&2, J.Found>): 130 match ps: 131 case []: 132 IO.pure(List<&2, J.Found>, []) 133 case +hd <> tl: 134 do IO>: 135 mm : Maybe<&2, String> <- F.read(hd) 136 rest : List<&2, J.Found> <- read.found(tl) 137 return J.Found{hd, F.text_of(mm)} <> rest 138 139 # the non-empty lines 140 def lines(ss: List<&2, String>) -> List<&2, String>: 141 match ss: 142 case []: 143 [] 144 case +hd <> tl: 145 +more = lines(tl) 146 Bool.pick(List<&2, String>, String.is_empty(hd), more, hd <> more) 147 148 # the ledger `ez lock` is planned from, what an upgrade needs to write its 149 # pins back, and the answers the lock reads the workspace's members from 150 type Planned is Data: 151 Planned{ledger: Maybe<&2, String>, back: U.Back, seeds: List<&2, LW.Reply>} 152 153 # the joint ledger, as the lock reads it, or the refusal that leaves none 154 def joint.at( 155 jj: J.Joint, 156 +text: String, 157 +spec: W.Spec, 158 +fs: List<&2, J.Found>, 159 +files: List<&2, K.Source>, 160 +lock: String 161 ) -> IO(Planned): 162 match jj: 163 case J.Lone{}: 164 IO.pure(Planned, Planned{Some{text}, U.Alone{}, []}) 165 case J.Clash{why}: 166 IO.die(Planned, 1, why) 167 case J.Joint{+mm}: 168 IO.pure(Planned, Planned{Some{Rend.show(mm)}, U.Back{text, spec, fs, lock}, 169 J.seeds(J.Joint{mm}, files)}) 170 171 # a workspace root's ledger joined with every member's, once git listed them 172 def joint.listed( 173 ok: Bool, 174 +text: String, 175 +spec: W.Spec, 176 +out: String, 177 +files: List<&2, K.Source> 178 ) -> IO(Planned): 179 match ok: 180 case True{}: 181 do IO: 182 +fs : List<&2, J.Found> <- read.found(lines(String.lines(out))) 183 lk : Maybe<&2, String> <- F.read("ez.lock.toml") 184 joint.at(J.joint(text, fs, files), text, spec, fs, files, F.text_of(lk)) 185 case False{}: 186 IO.die(Planned, 1, 187 "ez: a workspace is locked from the ez.toml files git tracks, and git could not list them: " ++ 188 String.trim(out)) 189 190 # a workspace root's ledger joined with every member's 191 def joint.go(+text: String, +spec: W.Spec, +files: List<&2, K.Source>) -> IO(Planned): 192 do IO: 193 +out : String <- Sn.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", ":(glob)**/ez.toml"]) 194 joint.listed(Sn.ok(out), text, spec, Sn.text(out), files) 195 196 # what one ledger's text says: no workspace, read as it is; a workspace root, 197 # whose members are read and joined; or a table that cannot be read, which 198 # the joint ledger refuses 199 def joint.read(rd: W.Read, +text: String, +files: List<&2, K.Source>) -> IO(Planned): 200 match rd: 201 case W.Lone{}: 202 IO.pure(Planned, Planned{Some{text}, U.Alone{}, []}) 203 case W.Bad{why}: 204 IO.die(Planned, 1, why) 205 case W.Root{+spec}: 206 joint.go(text, spec, files) 207 208 # the ledger `ez lock` is planned from: ez.toml as it is, or, at a workspace 209 # root, the joint ledger of the root and every member. `files` are the 210 # committed `.bend` files, which a member's package is walked over. 211 def ledger(led: Maybe<&2, String>, +files: List<&2, K.Source>) -> IO(Planned): 212 match led: 213 case None{}: 214 IO.pure(Planned, Planned{None{}, U.Alone{}, []}) 215 case Some{+text}: 216 joint.read(W.spec.of(text), text, files) 217 218 # --------------------------------------------------------------------------- 219 # a member's own commands 220 221 # an environment variable's text; unset, "" 222 def env.text(rr: Result<&1, &1, U32 & String, String>) -> String: 223 match rr: 224 case Fail{_e}: 225 "" 226 case Done{ss}: 227 String.trim(ss) 228 229 # BEND_LIB as the environment has it, "" for unset 230 def env.lib() -> IO(String): 231 do IO: 232 rr : Result<&1, &1, U32 & String, String> <- IO.get_env("BEND_LIB") 233 return env.text(rr) 234 235 # the same words run again where they are, with BEND_LIB set 236 def lib.go(+at: String, +pwd: String, xs: List<&2, String>) -> IO(U32): 237 match xs: 238 case []: 239 IO.pure(U32, 1) 240 case +prog <> rest: 241 Pass.run(H.lib.line(at, pwd, prog, rest)) 242 243 # a command acted on as the library decision says: where it is, or again 244 # where it is with BEND_LIB set to the root's 245 def lib.at(lib: H.Lib, go: Unit -> IO(Unit)) -> IO(Unit): 246 match lib: 247 case H.Keep{}: 248 go(Unit{}) 249 case H.Set{+at}: 250 do IO: 251 +pwd : String <- Sn.exec(["pwd"]) 252 xs : List<&2, String> <- argv() 253 +code : U32 <- lib.go(at, String.trim(Sn.text(pwd)), xs) 254 hop.end(U32.is_eq(code, 0), code) 255 256 # what the search found, acted on for a member's own command 257 def in.searched(st: Searched, +env: String, go: Unit -> IO(Unit)) -> IO(Unit): 258 Searched{+top, found} = st 259 lib.at(H.lib(top, found, env), go) 260 261 # a command that acts on a member's own ledger and entry, `ez add`, `ez 262 # remove`, `ez check`, `ez build`, `ez run` and `ez publish`: run where it is, 263 # with the root's library when it is run in a member 264 def in.member(go: Unit -> IO(Unit)) -> IO(Unit): 265 do IO: 266 st : Searched <- search() 267 +env : String <- env.lib() 268 in.searched(st, env, go) 269 270 # every listed ez.toml under a directory, read 271 def read.under(+dir: String, ps: List<&2, String>) -> IO(List<&2, J.Found>): 272 match ps: 273 case []: 274 IO.pure(List<&2, J.Found>, []) 275 case +hd <> tl: 276 do IO>: 277 mm : Maybe<&2, String> <- F.read(dir ++ "/" ++ hd) 278 rest : List<&2, J.Found> <- read.under(dir, tl) 279 return J.Found{hd, F.text_of(mm)} <> rest 280 281 # the joint ledger's dependencies, or none when it has none 282 def pins.joint(jj: J.Joint) -> List<&2, M.Dep>: 283 match jj: 284 case J.Joint{mm}: 285 M.Manifest{_n, _e, _b, _h, _pa, _pv, ds, _ts} = mm 286 ds 287 case _: 288 [] 289 290 # every listed file under a directory, read, as the walk takes it 291 def files.under(+dir: String, ps: List<&2, String>) -> IO(List<&2, K.Source>): 292 match ps: 293 case []: 294 IO.pure(List<&2, K.Source>, []) 295 case +hd <> tl: 296 do IO>: 297 mm : Maybe<&2, String> <- F.read(dir ++ "/" ++ hd) 298 rest : List<&2, K.Source> <- files.under(dir, tl) 299 return K.Source{hd, F.text_of(mm)} <> rest 300 301 # every dependency of the workspace a directory is in, as its root reads them 302 def pins.at(+dir: String) -> IO(List<&2, M.Dep>): 303 do IO>: 304 mm : Maybe<&2, String> <- F.read(dir ++ "/ez.toml") 305 +out : String <- Sn.exec(["git", "-C", dir, "-c", "core.quotepath=off", "ls-files", "--", 306 ":(glob)**/ez.toml"]) 307 fs : List<&2, J.Found> <- read.under(dir, lines(String.lines(Sn.text(out)))) 308 +bend : String <- Sn.exec(["git", "-C", dir, "-c", "core.quotepath=off", "ls-files", "--", "*.bend"]) 309 files : List<&2, K.Source> <- files.under(dir, lines(String.lines(Sn.text(bend)))) 310 return pins.joint(J.joint(F.text_of(mm), fs, files)) 311 312 # the ref, once the workspace's pins are known 313 def ref.held(rr: Pin.Ref) -> IO(String): 314 match rr: 315 case Pin.Given{ref}: 316 IO.pure(String, ref) 317 case Pin.Held{why}: 318 IO.die(String, 1, why) 319 320 # the ref an add goes on with, in what the search found 321 def ref.found(+top: String, found: R.Found, +home: String, +target: String, +ref: String) -> IO(String): 322 match found: 323 case R.Within{+root, +rel}: 324 do IO: 325 +pins : List<&2, M.Dep> <- pins.at(P.join(top, W.segs.text(root))) 326 ref.held(Pin.ref(pins, W.segs.text(rel), home, target, ref)) 327 case _: 328 IO.pure(String, ref) 329 330 # and once the search was made 331 def add.searched(st: Searched, +home: String, +target: String, +ref: String) -> IO(String): 332 Searched{+top, found} = st 333 ref.found(top, found, home, target, ref) 334 # the ref `ez add [ref]` goes on with: in a workspace, the pin the 335 # workspace already records for the source (EZ-WS-4) 336 def add.ref(+home: String, +target: String, +ref: String) -> IO(String): 337 do IO: 338 st : Searched <- search() 339 add.searched(st, home, target, ref) 340 341 342 # --------------------------------------------------------------------------- 343 # what `ez add` and `ez remove` read of the root 344 345 # every listed file under a directory, read, as the lock takes it 346 def sources.under(+dir: String, ps: List<&2, String>) -> IO(List<&2, LW.Source>): 347 match ps: 348 case []: 349 IO.pure(List<&2, LW.Source>, []) 350 case +hd <> tl: 351 do IO>: 352 mm : Maybe<&2, String> <- F.read(dir ++ "/" ++ hd) 353 rest : List<&2, LW.Source> <- sources.under(dir, tl) 354 return LW.Source{hd, F.text_of(mm)} <> rest 355 356 # the `.bend` files git lists under a directory, once it listed them 357 def listing.listed(ok: Bool, +dir: String, +out: String) -> IO(LW.Listing): 358 match ok: 359 case True{}: 360 do IO: 361 fs : List<&2, LW.Source> <- sources.under(dir, lines(String.lines(out))) 362 return LW.Listed{fs} 363 case False{}: 364 IO.pure(LW.Listing, LW.Unlisted{ 365 "ez: ez lock reads the .bend files git tracks, and git could not list them: " ++ String.trim(out)}) 366 367 # every `.bend` file git tracks under a directory, with its text, each path 368 # from it, as `ez lock` run there lists them 369 def listing.at(+dir: String) -> IO(LW.Listing): 370 do IO: 371 +out : String <- Sn.exec(["git", "-C", dir, "-c", "core.quotepath=off", "ls-files", "--", "*.bend"]) 372 listing.listed(Sn.ok(out), dir, Sn.text(out)) 373 374 # what a command run at `rel` from the root at `at` reads of the root: its 375 # ledger, every ez.toml and `.bend` file git lists under it, and its lock 376 def rooted(+at: String, rel: List<&2, String>) -> IO(Rel.Rooted): 377 do IO: 378 mm : Maybe<&2, String> <- F.read(at ++ "/ez.toml") 379 +out : String <- Sn.exec(["git", "-C", at, "-c", "core.quotepath=off", "ls-files", "--", 380 ":(glob)**/ez.toml"]) 381 fs : List<&2, J.Found> <- read.under(at, lines(String.lines(Sn.text(out)))) 382 ls : LW.Listing <- listing.at(at) 383 lk : Maybe<&2, String> <- F.read(at ++ "/ez.lock.toml") 384 return Rel.Rooted{at, rel, F.text_of(mm), fs, ls, F.text_of(lk)} 385 386 # where `ez add` or `ez remove` runs, in what the search found 387 def ws.found(+top: String, found: R.Found) -> IO(Rel.Ws): 388 match found: 389 case R.Within{root, rel}: 390 do IO: 391 rt : Rel.Rooted <- rooted(P.join(top, W.segs.text(root)), rel) 392 return Rel.Inside{rt} 393 case _: 394 IO.pure(Rel.Ws, Rel.Own{}) 395 396 # and once the search was made 397 def ws.searched(st: Searched) -> IO(Rel.Ws): 398 Searched{+top, found} = st 399 ws.found(top, found) 400 401 # where `ez add` or `ez remove` runs: in a project of its own, or in a 402 # workspace, with what it reads of the root (EZ-WS-10) 403 def relock.ws() -> IO(Rel.Ws): 404 do IO: 405 st : Searched <- search() 406 ws.searched(st) 407 408 # the lock, once the search was made 409 def lock.searched(st: Searched) -> String: 410 Searched{+top, found} = st 411 H.lock(top, found) 412 413 # the lock a member's own command reads: the root's, in a workspace 414 def lock.path() -> IO(String): 415 do IO: 416 st : Searched <- search() 417 return lock.searched(st) 418 419 # --------------------------------------------------------------------------- 420 # `ez doctor` at a workspace root 421 422 # a workspace root's ledger and every member's, read for the doctor 423 def doctor.root(+text: String) -> IO(Doc.Doctored): 424 do IO: 425 +out : String <- Sn.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", ":(glob)**/ez.toml"]) 426 +fs : List<&2, J.Found> <- read.found(lines(String.lines(Sn.text(out)))) 427 +bend : String <- Sn.exec(["git", "-c", "core.quotepath=off", "ls-files", "--", "*.bend"]) 428 files : List<&2, K.Source> <- files.under(".", lines(String.lines(Sn.text(bend)))) 429 return Doc.joint(J.joint(text, fs, files), text, fs) 430 431 # what one ledger's text says, for the doctor 432 def doctor.read(rd: W.Read, +text: String) -> IO(Doc.Doctored): 433 match rd: 434 case W.Lone{}: 435 IO.pure(Doc.Doctored, Doc.Doctored{Some{text}, Doc.Alone{}}) 436 case W.Bad{why}: 437 IO.pure(Doc.Doctored, Doc.Doctored{Some{text}, Doc.Back{text, [], why}}) 438 case W.Root{_spec}: 439 doctor.root(text) 440 441 # the ledger `ez doctor` judges: ez.toml as it is, or, at a workspace root, 442 # the joint ledger of the root and every member 443 def doctor.ledger(led: Maybe<&2, String>) -> IO(Doc.Doctored): 444 match led: 445 case None{}: 446 IO.pure(Doc.Doctored, Doc.Doctored{None{}, Doc.Alone{}}) 447 case Some{+text}: 448 doctor.read(W.spec.of(text), text)