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 0xabe575924687afad4cee1a2c1194d639/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.Text{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 loop, under fuel. Every round adds an answer, since the planner never 278 # asks what the World answers already, and it asks at most five rounds, so 279 # the fuel is only a guard. 280 def loop(fuel: Nat, +world: TW.World) -> IO(Unit): 281 match fuel: 282 case 0n: 283 IO.die(Unit, 1, "ez: ez tool kept asking questions") 284 case 1n+f: 285 loop.step(TP.step(world), world, w => loop(f, w)) 286 287 # a variable, "" when it is unset 288 def var(name: String) -> IO(String): 289 do IO: 290 r : Result<&1, &1, U32 & String, String> <- IO.get_env(name) 291 return Run.env.text(r) 292 293 # the bend on the PATH, as its banner's first line. A bend that does not 294 # answer is "", and its build fails on its own. 295 def bend.is() -> IO(String): 296 do IO: 297 +out : String <- Bend.version() 298 return Bool.pick(String, R.ok(out), line0(String.lines(String.trim(R.text(out)))), "") 299 300 # the variables the tool commands read 301 def env() -> IO(TW.Env): 302 do IO: 303 +b : String <- var("EZ_TOOL_BIN") 304 +x : String <- var("XDG_BIN_HOME") 305 +c : String <- var("XDG_CACHE_HOME") 306 +h : String <- var("HOME") 307 +p : String <- var("PATH") 308 return TW.Env{b, x, c, h, p} 309 310 # one target, run, installed or upgraded, over the project's ledger and lock, 311 # building the file `want` names when it names one 312 def go( 313 act: TW.Act, 314 +target: String, 315 +want: String, 316 +here: String, 317 +vars: TW.Env, 318 +bend: String, 319 +led: Maybe<&2, String>, 320 +lock: Maybe<&2, String> 321 ) -> IO(Unit): 322 loop(U32.to_nat(100), TW.World{act, target, want, here, vars, bend, led, lock, [], []}) 323 324 # `ez tool run [--entry ] [-- args…]`, `install` and `upgrade` 325 def run(act: TW.Act, +target: String, +want: String) -> IO(Unit): 326 do IO: 327 +pwd : String <- R.exec(["pwd"]) 328 +vars : TW.Env <- env() 329 +bend : String <- bend.is() 330 led : Maybe<&2, String> <- F.read("ez.toml") 331 lock : Maybe<&2, String> <- F.read("ez.lock.toml") 332 go(act, target, want, String.trim(R.text(pwd)), vars, bend, led, lock) 333 334 # every name a sync installs, in turn 335 def sync.all( 336 ns: List<&2, String>, 337 +here: String, 338 +vars: TW.Env, 339 +bend: String, 340 +led: Maybe<&2, String>, 341 +lock: Maybe<&2, String> 342 ) -> IO(Unit): 343 match ns: 344 case []: 345 IO.pure(Unit, Unit{}) 346 case +h <> t: 347 do IO: 348 go(TW.Install{}, h, "", here, vars, bend, led, lock) 349 sync.all(t, here, vars, bend, led, lock) 350 351 # a sync's names installed in turn, none when it refuses, and then its end 352 # (`TP.sync.outcome`) 353 def sync.go( 354 +synced: TP.Sync, 355 +here: String, 356 +vars: TW.Env, 357 +bend: String, 358 +led: Maybe<&2, String>, 359 +lock: Maybe<&2, String> 360 ) -> IO(Unit): 361 do IO: 362 sync.all(TP.sync.names(synced), here, vars, bend, led, lock) 363 Run.end(TP.sync.outcome(synced)) 364 365 # `ez tool sync`: every pin the ledger names, installed at its lock rev 366 def sync() -> IO(Unit): 367 do IO: 368 +pwd : String <- R.exec(["pwd"]) 369 +vars : TW.Env <- env() 370 +bend : String <- bend.is() 371 +led : Maybe<&2, String> <- F.read("ez.toml") 372 +lock : Maybe<&2, String> <- F.read("ez.lock.toml") 373 sync.go(TP.sync(led, lock), String.trim(R.text(pwd)), vars, bend, led, lock)