1 # ez/cmd: what each subcommand does. Everything that touches the network or the 2 # file system goes through a process effect, because bend links no TLS and 3 # Base has neither mkdir nor readdir: snap's `exec` for a program whose answer 4 # ez reads, and `share/pass.bend` for the one `ez run` hands the terminal to. 5 import Base 6 import 0x8e26a6f8c31689519e618374376ee3dd/main.bend as R 7 import ../io/file.bend as F 8 import ../lock/run.bend as Run 9 import ../lock/plan.bend as P 10 import ../share/env.bend as Env 11 import ../share/exec.bend as Exec 12 import ../share/cap.bend as Cap 13 import ../share/args.bend as Args 14 import ../ez/ends.bend as E 15 import ../ez/start.bend as S 16 import ../share/pass.bend as Pass 17 import ../init/run.bend as InitRun 18 import ../remove/run.bend as RemoveRun 19 import ../add/run.bend as AddRun 20 import ../fetch/run.bend as FetchRun 21 import ../pub/run.bend as PubRun 22 23 # the ledger's path, which is fixed 24 def toml() -> String: 25 "ez.toml" 26 27 # what `ez check`, `ez build` or `ez run` does, decided from ez.toml as it is 28 # on disk (`S.start`): a missing or unreadable ledger is refused before 29 # anything is started or made 30 def started(+ask: S.Ask) -> IO(S.Start): 31 do IO: 32 t : Maybe<&2, String> <- F.read(toml()) 33 return S.start(ask, t) 34 35 # a refusal said, and the command ended on it with exit 1 36 def refused(+why: String) -> IO(Unit): 37 Run.end(P.Refused{why}) 38 39 # a build's output, printed with a word in front of it for a run the kernel 40 # ended rather than the program, and the command ended on it (`E.ran`): a 41 # failed build must fail the command, or a script around it learns nothing. 42 # bend prints that all terms check before it emits any C, so a build killed at 43 # the cap is otherwise indistinguishable from one that worked and wrote 44 # nothing. 45 def report.capped(+why: String, +out: String) -> IO(Unit): 46 do IO: 47 IO.write(why ++ R.text(out)) 48 Run.end(E.ran(out)) 49 50 # `ez check` and `ez run` are not capped, and `ez build` is. The wall 51 # `share/cap.bend` exists for is bend's C backend: emitting the C for a large 52 # program peaked at 18.8 GB here and an uncapped one reached 35 GB. `ez check` 53 # emits JS and `ez run` interprets, so neither reaches that backend; and what 54 # `ez run` spends after that is the user's own program spending it, which is 55 # not ez's to ration. 56 # 57 # Whether a check passed is `E.check.passed`: bend exited 0, or said only 58 # that the entry has no main. 59 60 # a passing check says so, since bend says nothing when it is happy, and a 61 # failing one shows what bend said 62 def check.said(passed: Bool, entry: String, text: String) -> IO(Unit): 63 match passed: 64 case True{}: 65 IO.print("ok " ++ entry) 66 case False{}: 67 IO.write(text) 68 69 # the entry checked, and never run, once `.ez` is there for its JS. The 70 # entry is the word after `bend` on the line. 71 def check.go(start: S.Start) -> IO(Unit): 72 match start: 73 case S.Refused{why}: 74 refused(why) 75 case S.Starts{+line}: 76 do IO: 77 Exec.make(Env.dirs("check", "")) 78 +out : String <- Exec.exec(line) 79 check.said(E.check.passed(out), Args.at(line, 1n), R.text(out)) 80 Run.end(E.checked(out)) 81 82 # `ez check` 83 def check() -> IO(Unit): 84 do IO: 85 start : S.Start <- started(S.Check{}) 86 check.go(start) 87 88 # a native binary, named after the package (`S.build.out`), once the 89 # directory it lands in is there. This is the one command that reaches bend's 90 # C backend, so it is the one that runs under the memory cap. 91 def build.go(start: S.Start, +out: String) -> IO(Unit): 92 match start: 93 case S.Refused{why}: 94 refused(why) 95 case S.Starts{line}: 96 do IO: 97 Exec.make(Env.dirs("build", out)) 98 +cap : Bool <- Cap.ok() 99 Cap.warn(cap) 100 +g : String <- Cap.gb() 101 +bin : String <- Cap.run(cap, line) 102 report.capped(Cap.why(bin, g), bin) 103 104 # `ez build [out]` 105 def build(+out: String) -> IO(Unit): 106 do IO: 107 start : S.Start <- started(S.Build{out}) 108 build.go(start, out) 109 110 # the program `ez run` starts, on ez's own stdin, stdout and stderr, with the 111 # project's BEND_LIB set for it, and the status it exited with; 0 when the 112 # run was refused, which starts none 113 def run.started(start: S.Start, +at: String) -> IO(U32): 114 match start: 115 case S.Refused{_why}: 116 IO.pure(U32, 0) 117 case S.Starts{line}: 118 Pass.run(Env.line(at, line)) 119 120 # a run, and how it ends: exit 1 with the reason for a refusal, and the 121 # program's own status otherwise (`S.code`, EZ-OUT-1) 122 def run.go(+start: S.Start) -> IO(Unit): 123 do IO: 124 at : String <- Env.lib() 125 +program : U32 <- run.started(start, at) 126 +code : U32 <- IO.pure(U32, S.code(start, program)) 127 Run.end.at(U32.is_eq(code, 0), code, S.why(start)) 128 129 # `ez run [args..]`: the entry run by bend, which checks it first, with every 130 # word after `run`, as `cargo run` runs a project's binary 131 def run(as: List<&2, String>) -> IO(Unit): 132 do IO: 133 +start : S.Start <- started(S.Run{as}) 134 run.go(start) 135 136 # `ez publish`, planned by pub/plan.bend: the entry the ledger names, sent to 137 # the hub once the tree is clean and git tracks every file of the package, 138 # and bend's answer checked against the `0x` name ez computed. A ledger that 139 # is missing or does not parse is refused, and nothing is sent. 140 def publish() -> IO(Unit): 141 PubRun.run() 142 143 # `ez lock`, plain or `--upgrade`, is the planner's 144 # (lock/plan.bend, lock/up.bend): a plain lock copies every pin as the ledger 145 # has it, refuses one it would have to fill, and never writes ez.toml; an 146 # upgrade moves the selected pins, writes ez.toml once, `.gitignore`, the 147 # rewritten imports and the lock in one plan. Either writes nothing when it 148 # refuses, and a missing ledger is refused. `--package` is only a filter on an 149 # upgrade. 150 # `--package` without `--upgrade` is refused before anything is read 151 # (`E.lock.flags`). 152 def lock.go(flags: P.Outcome, up: Bool, +name: String) -> IO(Unit): 153 match flags: 154 case P.Success{}: 155 Run.run(up, name) 156 case P.Refused{why}: 157 Run.end(P.Refused{why}) 158 159 # `ez lock` and `ez lock --upgrade [--package NAME]` 160 def lock(+up: Bool, +name: String) -> IO(Unit): 161 lock.go(E.lock.flags(up, name), up, name) 162 163 # `ez fetch`, planned by fetch/plan.bend: BEND_LIB filled from the lock, with 164 # nothing re-resolved, and nothing laid unless every package checks 165 def fetch() -> IO(Unit): 166 FetchRun.run() 167 168 # `ez init [name] [entry.bend] [--description TEXT]`: a new project, planned 169 # by init/plan.bend 170 def init(+asked: String, +file: String, +desc: String) -> IO(Unit): 171 InitRun.run(asked, file, desc) 172 173 # `ez add [rev|tag|branch] [entry.bend] [--rename NAME]`, planned by 174 # add/plan.bend. The target is an `owner/repo`, a git url, or a path, the same 175 # shapes `ez tool run` takes, or a hub package's `@` 176 # (add/hub.bend). A missing ref is resolved, and a missing entry is read from 177 # the checkout, or from the package the hub serves. 178 def add(+target: String, +ref: String, +entry: String, +rename: String) -> IO(Unit): 179 AddRun.run(target, ref, entry, rename) 180 181 # `ez remove `, planned by remove/plan.bend 182 def remove(+name: String) -> IO(Unit): 183 RemoveRun.run(name)