1 # tool/run: the interpreter for `ez tool run`, `install`, `upgrade` and 2 # `sync`. It reads the directory it runs in, the variables, the bend on the 3 # PATH, and the project's ez.toml and ez.lock.toml into a World, then loops: 4 # it asks the planner (tool/plan.bend) what is still open, answers each 5 # question by IO, and adds the answers to the World, until nothing is left 6 # to ask. Then it runs the plan: the checkout laid, the binary built or 7 # reused, then the program started or the link written, and the plan's end. 8 # A git question is the lock interpreter's to answer (`Run.answer.ups`), a 9 # file is written by its `Run.write`, and the command exits with the status 10 # the planner gives the plan's end and the program's (`TP.code`), through 11 # `Run.end.at`. It decides nothing; that it reads and executes faithfully is 12 # EZ-TRUST-2. 13 # 14 # What it asks and builds on the way is captured, and a build's output is 15 # shown on stderr only when the build fails. The program alone is started on 16 # ez's own stdin, stdout and stderr (`Pass.run`), as `cargo run` hands the 17 # terminal to the program once the build is done. 18 # 19 # `sync` asks the planner which pins to install (`TP.sync`) and installs 20 # each in turn as `ez tool install ` would. 21 import Base 22 import 0xead4c7939c9eaca8d47da53b4496b731/main.bend as R 23 import ../io/file.bend as F 24 import ../lock/run.bend as Run 25 import ../lock/plan.bend as LP 26 import ../lock/up.bend as Up 27 import ../git/git.bend as Git 28 import ../fetch/run.bend as FetchRun 29 import ../pkg/path.bend as P 30 import ../run/bend.bend as Bend 31 import ../share/cap.bend as Cap 32 import ../share/say.bend as Say 33 import ../share/spin.bend as Spin 34 import ../share/pass.bend as Pass 35 import ./world.bend as TW 36 import ./plan.bend as TP 37 38 # --------------------------------------------------------------------------- 39 # answering 40 41 # the first line, or nothing 42 def line0(ls: List<&2, String>) -> String: 43 match ls: 44 case []: 45 "" 46 case h <> _t: 47 h 48 49 # git can write a `warning:` line to stderr and still exit 0, and snap 50 # answers stdout and stderr together (SNAP-ANS-4), so an answer is read by the 51 # shape of the line it wants, never by where that line stands. 52 53 # the line found, or the walk gone on past a line of another shape 54 def said.next(hit: Bool, +found: String, go: Unit -> String) -> String: 55 match hit: 56 case True{}: 57 found 58 case False{}: 59 go(Unit{}) 60 61 # the first line that is a commit, or "" 62 def said.rev(ls: List<&2, String>) -> String: 63 match ls: 64 case []: 65 "" 66 case +h <> t: 67 said.next(Git.is_rev(h), h, _u => said.rev(t)) 68 69 # a line that is an absolute path, as `rev-parse --show-toplevel` prints one 70 def line.abs(+line: String) -> Bool: 71 String.eq(String.take(line, 1n), "/") 72 73 # the first line that is an absolute path, or "" 74 def said.abs(ls: List<&2, String>) -> String: 75 match ls: 76 case []: 77 "" 78 case +h <> t: 79 said.next(line.abs(h), h, _u => said.abs(t)) 80 81 # what a command that succeeded answered, read only then; "" when it failed 82 def said.when(ok: Bool, read: Unit -> String) -> String: 83 match ok: 84 case True{}: 85 read(Unit{}) 86 case False{}: 87 "" 88 89 # the commit `git rev-parse HEAD` answered, or "" when it failed 90 def said.head(+out: String) -> String: 91 said.when(R.ok(out), _u => said.rev(String.lines(R.text(out)))) 92 93 # the top `git rev-parse --show-toplevel` answered, or "" when it failed 94 def said.top(+out: String) -> String: 95 said.when(R.ok(out), _u => said.abs(String.lines(R.text(out)))) 96 97 # what git says of a path that has a real path: the top of its checkout, 98 # HEAD, and what `git status` names, untracked files included whatever the 99 # repository's own settings hide 100 def located(+abs: String) -> IO(TW.Answer): 101 do IO: 102 +top : String <- R.exec(["git", "-C", abs, "rev-parse", "--show-toplevel"]) 103 +head : String <- R.exec(["git", "-C", abs, "rev-parse", "HEAD"]) 104 +st : String <- R.exec(["git", "-C", abs, "status", "--porcelain", 105 "--untracked-files=normal"]) 106 return TW.Located{abs, said.top(top), said.head(head), R.ok(st), 107 String.trim(R.text(st))} 108 109 # a path that has a real path, or why it has none 110 def locate.real(ok: Bool, +path: String, +text: String) -> IO(TW.Answer): 111 match ok: 112 case False{}: 113 IO.pure(TW.Answer, TW.Miss{"ez: " ++ path ++ ": " ++ String.trim(text)}) 114 case True{}: 115 located(String.trim(text)) 116 117 # a local path, made real, and what git says of it 118 def locate(+path: String) -> IO(TW.Answer): 119 do IO: 120 +out : String <- R.exec(["realpath", path]) 121 locate.real(R.ok(out), path, R.text(out)) 122 123 # one question answered 124 def answer(ask: TW.Ask) -> IO(TW.Answer): 125 match ask: 126 case TW.Read{path}: 127 do IO: 128 m : Maybe<&2, String> <- F.read(path) 129 return TW.Plain{m} 130 case TW.Exec{path}: 131 do IO: 132 +out : String <- R.exec(["test", "-x", path]) 133 return TW.Is{R.ok(out)} 134 case TW.Locate{path}: 135 locate(path) 136 137 # every question answered 138 def answer.all(asks: List<&2, TW.Ask>) -> IO(List<&2, TW.Reply>): 139 match asks: 140 case []: 141 IO.pure(List<&2, TW.Reply>, []) 142 case +h <> t: 143 do IO>: 144 a : TW.Answer <- answer(h) 145 rest : List<&2, TW.Reply> <- answer.all(t) 146 return TW.Reply{h, a} <> rest 147 148 # the World with more answers in it 149 def more(world: TW.World, rs: List<&2, TW.Reply>, us: List<&2, Up.Reply>) -> TW.World: 150 TW.World{a, t, want, here, env, bend, ledger, lock, replies, ups} = world 151 TW.World{a, t, want, here, env, bend, ledger, lock, List.append(&2, TW.Reply, replies, rs), 152 List.append(&2, Up.Reply, ups, us)} 153 154 # --------------------------------------------------------------------------- 155 # executing 156 157 # a checkout laid: whatever was there removed, then every file, the record 158 # last 159 def exec.lay(layout: TP.Lay) -> IO(Unit): 160 match layout: 161 case TP.Still{}: 162 IO.pure(Unit, Unit{}) 163 case TP.Lay{dir, ws}: 164 do IO: 165 _rm : String <- R.exec(["rm", "-rf", dir]) 166 Run.exec.all("", ws) 167 168 # a build's output, shown on stderr when it failed, since stdout is the 169 # program's, as cargo keeps its own words off it 170 def compile.said(ok: Bool, +why: String, +text: String) -> IO(Unit): 171 match ok: 172 case True{}: 173 IO.pure(Unit, Unit{}) 174 case False{}: 175 do IO: 176 IO.print_err(String.trim_end(why ++ text)) 177 IO.die(Unit, 1, "") 178 179 # `bend -o ` under the memory cap, with the checkout's library 180 def compile(+lib: String, +file: String, +out: String) -> IO(Unit): 181 do IO: 182 +on : Bool <- Cap.ok() 183 Cap.warn.err(on) 184 +g : String <- Cap.gb() 185 _mk : String <- R.exec(["mkdir", "-p", P.dir(out)]) 186 +bin : String <- R.exec(Cap.argv(on, g, lib, ["bend", file, "-o", out])) 187 compile.said(R.ok(bin), Cap.why(bin, g), R.text(bin)) 188 189 # the library a build reads: an ez project's lock fetched into it as `ez 190 # fetch` fetches one, with every relative path in it read from the 191 # checkout; a plain repository's made, for bend to fetch its hub imports into 192 def exec.lib(fetch: Bool, +top: String, +lib: String) -> IO(Unit): 193 match fetch: 194 case True{}: 195 FetchRun.fill(top, lib) 196 case False{}: 197 do IO: 198 _mk : String <- R.exec(["mkdir", "-p", lib]) 199 return Unit{} 200 201 # the binary: its library filled, the file built, the record written; or the 202 # one there reused 203 def exec.make(making: TP.Make) -> IO(Unit): 204 match making: 205 case TP.Skip{}: 206 IO.pure(Unit, Unit{}) 207 case TP.Reuse{name}: 208 IO.print_err(Say.reused(name)) 209 case TP.Make{+top, +lib, fetch, file, out, at, key, +name}: 210 do IO: 211 exec.lib(fetch, top, lib) 212 +s : Say.Spin <- Spin.hold(Say.building(name)) 213 compile(lib, file, out) 214 Spin.done(s) 215 Run.write(at, key) 216 217 # the lines a person is told, each on stderr 218 def tell(ls: List<&2, String>) -> IO(Unit): 219 match ls: 220 case []: 221 IO.pure(Unit, Unit{}) 222 case h <> t: 223 do IO: 224 IO.print_err(h) 225 tell(t) 226 227 def link.after(ok: Bool, +at: String, +text: String, says: List<&2, String>) -> IO(Unit): 228 match ok: 229 case False{}: 230 IO.die(Unit, 1, "ez: " ++ at ++ ": " ++ String.trim(text)) 231 case True{}: 232 tell(says) 233 234 # the program started with its words on ez's own stdin, stdout and stderr, 235 # or the link written, and the status the program exited with, 0 when none 236 # was started. What the program prints reaches the person as it prints it, 237 # and it reads what ez would have read. 238 def exec.finish(fin: TP.Finish) -> IO(U32): 239 match fin: 240 case TP.Idle{}: 241 IO.pure(U32, 0) 242 case TP.Launch{bin, args}: 243 Pass.run(List.append(&2, String, [bin], args)) 244 case TP.Link{bin, +dir, +name, says}: 245 +at = dir ++ "/" ++ name 246 do IO: 247 IO.print_err(Say.linking(at)) 248 _mk : String <- R.exec(["mkdir", "-p", dir]) 249 +ln : String <- R.exec(["ln", "-sfn", bin, at]) 250 link.after(R.ok(ln), at, R.text(ln), says) 251 return 0 252 253 # a plan, executed, and how it ends: with the status the planner gives its 254 # end and the program's status (`TP.code`, EZ-OUT-1) 255 def exec.plan(+pl: TP.Plan) -> IO(Unit): 256 TP.Plan{l, m, f, o} = pl 257 do IO: 258 exec.lay(l) 259 exec.make(m) 260 s : U32 <- exec.finish(f) 261 +code : U32 <- IO.pure(U32, TP.code(TP.exit(pl), s)) 262 Run.end.at(U32.is_eq(code, 0), code, LP.why(o)) 263 264 # --------------------------------------------------------------------------- 265 # the loop 266 267 def loop.step(st: TP.Step, world: TW.World, go: TW.World -> IO(Unit)) -> IO(Unit): 268 match st: 269 case TP.Asking{asks, ups}: 270 do IO: 271 rs : List<&2, TW.Reply> <- answer.all(asks) 272 us : List<&2, Up.Reply> <- Run.answer.ups("", ups) 273 go(more(world, rs, us)) 274 case TP.Run{pl}: 275 exec.plan(pl) 276 277 # the rounds the loop may take. The planner asks at most five rounds, so this 278 # is only a guard against a planner that keeps asking, and running out of it 279 # dies loudly. 280 def rounds() -> Nat: 281 U32.to_nat(100) 282 283 # the loop, under fuel. Every round adds an answer, since the planner never 284 # asks what the World answers already, and it asks at most five rounds, so 285 # the fuel is only a guard. 286 def loop(fuel: Nat, +world: TW.World) -> IO(Unit): 287 match fuel: 288 case 0n: 289 IO.die(Unit, 1, "ez: ez tool kept asking questions") 290 case 1n+f: 291 loop.step(TP.step(world), world, w => loop(f, w)) 292 293 # a variable, "" when it is unset 294 def var(name: String) -> IO(String): 295 do IO: 296 r : Result<&1, &1, U32 & String, String> <- IO.get_env(name) 297 return Run.env.text(r) 298 299 # the bend on the PATH, as its banner's first line. A bend that does not 300 # answer is "", and its build fails on its own. 301 def bend.is() -> IO(String): 302 do IO: 303 +out : String <- Bend.version() 304 return Bool.pick(String, R.ok(out), line0(String.lines(String.trim(R.text(out)))), "") 305 306 # the variables the tool commands read 307 def env() -> IO(TW.Env): 308 do IO: 309 +b : String <- var("EZ_TOOL_BIN") 310 +x : String <- var("XDG_BIN_HOME") 311 +c : String <- var("XDG_CACHE_HOME") 312 +h : String <- var("HOME") 313 +p : String <- var("PATH") 314 return TW.Env{b, x, c, h, p} 315 316 # one target, run, installed or upgraded, over the project's ledger and lock, 317 # building the file `want` names when it names one 318 def go( 319 act: TW.Act, 320 +target: String, 321 +want: String, 322 +here: String, 323 +vars: TW.Env, 324 +bend: String, 325 +led: Maybe<&2, String>, 326 +lock: Maybe<&2, String> 327 ) -> IO(Unit): 328 loop(rounds(), TW.World{act, target, want, here, vars, bend, led, lock, [], []}) 329 330 # `ez tool run [--entry ] [-- args…]`, `install` and `upgrade` 331 def run(act: TW.Act, +target: String, +want: String) -> IO(Unit): 332 do IO: 333 +pwd : String <- R.exec(["pwd"]) 334 +vars : TW.Env <- env() 335 +bend : String <- bend.is() 336 led : Maybe<&2, String> <- F.read("ez.toml") 337 lock : Maybe<&2, String> <- F.read("ez.lock.toml") 338 go(act, target, want, String.trim(R.text(pwd)), vars, bend, led, lock) 339 340 # every name a sync installs, in turn 341 def sync.all( 342 ns: List<&2, String>, 343 +here: String, 344 +vars: TW.Env, 345 +bend: String, 346 +led: Maybe<&2, String>, 347 +lock: Maybe<&2, String> 348 ) -> IO(Unit): 349 match ns: 350 case []: 351 IO.pure(Unit, Unit{}) 352 case +h <> t: 353 do IO: 354 go(TW.Install{}, h, "", here, vars, bend, led, lock) 355 sync.all(t, here, vars, bend, led, lock) 356 357 # a sync's names installed in turn, none when it refuses, and then its end 358 # (`TP.sync.outcome`) 359 def sync.go( 360 +synced: TP.Sync, 361 +here: String, 362 +vars: TW.Env, 363 +bend: String, 364 +led: Maybe<&2, String>, 365 +lock: Maybe<&2, String> 366 ) -> IO(Unit): 367 do IO: 368 sync.all(TP.sync.names(synced), here, vars, bend, led, lock) 369 Run.end(TP.sync.outcome(synced)) 370 371 # `ez tool sync`: every pin the ledger names, installed at its lock rev 372 def sync() -> IO(Unit): 373 do IO: 374 +pwd : String <- R.exec(["pwd"]) 375 +vars : TW.Env <- env() 376 +bend : String <- bend.is() 377 +led : Maybe<&2, String> <- F.read("ez.toml") 378 +lock : Maybe<&2, String> <- F.read("ez.lock.toml") 379 sync.go(TP.sync(led, lock), String.trim(R.text(pwd)), vars, bend, led, lock)