1 # tool/world: what `ez tool run`, `install` and `upgrade` read. The act and 2 # the target they were given; the directory they run in (`here`), which a 3 # relative cache root is taken from; the variables that place the cache and 4 # the link (`EZ_TOOL_BIN`, `XDG_BIN_HOME`, `XDG_CACHE_HOME`, `HOME`, `PATH`); 5 # the bend on the PATH, as its banner's first line; the file `--entry` names, 6 # "" when it names none; the project's ledger and lock, None when there is 7 # none; and the answers to the questions the planner (tool/plan.bend) 8 # asked. `ez tool sync` reads only the ledger and the lock. 9 # 10 # A question is a file's text, whether a binary is there, what git says of a 11 # local path, or one of the upgrade's (lock/up.bend): `Head` for the commit a 12 # remote's HEAD names, and `Clone` for the checkout at a rev with every 13 # file's text. The upgrade's are answered by the lock's interpreter and kept 14 # apart, in `ups`, so its lookups serve here as they are. 15 # 16 # Everything else here is a projection: what a World says, read one way. 17 import Base 18 import ../lock/up.bend as Up 19 import ../ledger/manifest.bend as M 20 import ../pkg/path.bend as P 21 22 # what the command does with the binary once it is current: `Run` starts it 23 # with these words, `Install` and `Upgrade` link it onto PATH 24 type Act is Data: 25 Run{args: List<&2, String>} 26 Install{} 27 Upgrade{} 28 29 # the variables the command reads, each "" when it is unset 30 type Env is Data: 31 Env{tool_bin: String, xdg_bin: String, xdg_cache: String, home: String, path: String} 32 33 # a question the planner asks that is not git's about a remote 34 type Ask is Data: 35 Read{path: String} # a file's text, None when there is none 36 Exec{path: String} # whether a file is there and executable 37 Locate{path: String} # what git says of a local path 38 39 # the answer to one: a file's text, whether a binary is there, or what git 40 # said of a path: its real path, the top of the checkout holding it, the 41 # commit HEAD names, and whether `git status` answered and what it printed. 42 # A path that has no real path, or a question IO could not answer, is a miss. 43 type Answer is Data: 44 Plain{got: Maybe<&2, String>} 45 Is{yes: Bool} 46 Located{abs: String, top: String, head: String, ok: Bool, status: String} 47 Miss{why: String} 48 49 # one question, answered 50 type Reply is Data: 51 Reply{ask: Ask, answer: Answer} 52 53 # everything `ez tool run`, `install` and `upgrade` read 54 type World is Data: 55 World{act: Act, target: String, want: String, here: String, env: Env, bend: String, 56 ledger: Maybe<&2, String>, lock: Maybe<&2, String>, replies: List<&2, Reply>, 57 ups: List<&2, Up.Reply>} 58 59 # the act 60 def act(world: World) -> Act: 61 World{a, _t, _want, _here, _env, _bend, _ledger, _lock, _rs, _ups} = world 62 a 63 64 # the target asked for 65 def target(world: World) -> String: 66 World{_a, t, _want, _here, _env, _bend, _ledger, _lock, _rs, _ups} = world 67 t 68 69 # the file `--entry` names, "" when it names none 70 def want(world: World) -> String: 71 World{_a, _t, w, _here, _env, _bend, _ledger, _lock, _rs, _ups} = world 72 w 73 74 # the directory ez runs in 75 def here(world: World) -> String: 76 World{_a, _t, _want, h, _env, _bend, _ledger, _lock, _rs, _ups} = world 77 h 78 79 # the variables 80 def env(world: World) -> Env: 81 World{_a, _t, _want, _here, e, _bend, _ledger, _lock, _rs, _ups} = world 82 e 83 84 # the bend on the PATH, "" when it did not answer 85 def bend(world: World) -> String: 86 World{_a, _t, _want, _here, _env, b, _ledger, _lock, _rs, _ups} = world 87 b 88 89 # the project's ez.toml, None when there is none 90 def ledger(world: World) -> Maybe<&2, String>: 91 World{_a, _t, _want, _here, _env, _bend, l, _lock, _rs, _ups} = world 92 l 93 94 # the project's ez.lock.toml, None when there is none 95 def lock(world: World) -> Maybe<&2, String>: 96 World{_a, _t, _want, _here, _env, _bend, _ledger, l, _rs, _ups} = world 97 l 98 99 # every question of ours answered so far 100 def replies(world: World) -> List<&2, Reply>: 101 World{_a, _t, _want, _here, _env, _bend, _ledger, _lock, rs, _ups} = world 102 rs 103 104 # every one of the upgrade's questions answered so far 105 def ups(world: World) -> List<&2, Up.Reply>: 106 World{_a, _t, _want, _here, _env, _bend, _ledger, _lock, _rs, us} = world 107 us 108 109 def env.tool_bin(vars: Env) -> String: 110 Env{b, _x, _c, _h, _p} = vars 111 b 112 113 def env.xdg_bin(vars: Env) -> String: 114 Env{_b, x, _c, _h, _p} = vars 115 x 116 117 def env.xdg_cache(vars: Env) -> String: 118 Env{_b, _x, c, _h, _p} = vars 119 c 120 121 def env.home(vars: Env) -> String: 122 Env{_b, _x, _c, h, _p} = vars 123 h 124 125 def env.path(vars: Env) -> String: 126 Env{_b, _x, _c, _h, p} = vars 127 p 128 129 # HOME 130 def home(world: World) -> String: 131 env.home(env(world)) 132 133 # --------------------------------------------------------------------------- 134 # where things go 135 136 # `~/.local/bin`, when there is a HOME 137 def bin.home(+home: String) -> Maybe<&2, String>: 138 Bool.pick(Maybe<&2, String>, String.is_empty(home), None{}, Some{home ++ "/.local/bin"}) 139 140 # `$XDG_BIN_HOME` when it is set, otherwise the one above 141 def bin.xdg(+xdg: String, +home: String) -> Maybe<&2, String>: 142 Bool.pick(Maybe<&2, String>, String.is_empty(xdg), bin.home(home), Some{xdg}) 143 144 # where an installed command is linked: `$EZ_TOOL_BIN` when it is set, 145 # otherwise `$XDG_BIN_HOME`, otherwise `$HOME/.local/bin`, an empty value 146 # counting as unset; None when all three are empty (EZ-TOOL-1) 147 def bin.dir(+ez: String, +xdg: String, +home: String) -> Maybe<&2, String>: 148 Bool.pick(Maybe<&2, String>, String.is_empty(ez), bin.xdg(xdg, home), Some{ez}) 149 150 # the link directory of a World's variables 151 def bin.of(vars: Env) -> Maybe<&2, String>: 152 Env{+b, +x, _c, +h, _p} = vars 153 bin.dir(b, x, h) 154 155 # the cache root: `$XDG_CACHE_HOME/ez/tool`, otherwise `$HOME/.cache/ez/tool`, 156 # taken from where ez runs when it is relative, so every path the plan names 157 # is absolute; None when neither variable is set 158 def cache.home(+here: String, +home: String) -> Maybe<&2, String>: 159 Bool.pick(Maybe<&2, String>, String.is_empty(home), None{}, 160 Some{P.anchor(here, home) ++ "/.cache/ez/tool"}) 161 162 def cache.dir(+here: String, +xdg: String, +home: String) -> Maybe<&2, String>: 163 Bool.pick(Maybe<&2, String>, String.is_empty(xdg), cache.home(here, home), 164 Some{P.anchor(here, xdg) ++ "/ez/tool"}) 165 166 # the cache root of a World 167 def cache.of(world: World) -> Maybe<&2, String>: 168 World{_a, _t, _want, +here, vars, _bend, _ledger, _lock, _rs, _ups} = world 169 Env{_b, _x, +c, +h, _p} = vars 170 cache.dir(here, c, h) 171 172 # a Maybe's text, "" for none 173 def text.or(got: Maybe<&2, String>) -> String: 174 match got: 175 case None{}: 176 "" 177 case Some{t}: 178 t 179 180 # --------------------------------------------------------------------------- 181 # the project 182 183 # the project's ledger as read. A project with no ez.toml pins nothing, so 184 # every target is a free one; one that does not parse is its own refusal. 185 def project.of(got: Maybe<&2, String>) -> M.Read: 186 match got: 187 case None{}: 188 M.Good{M.Manifest{"", "", "", "", "", "", [], []}} 189 case Some{text}: 190 M.parse(text) 191 192 # the project's ledger as read 193 def project(world: World) -> M.Read: 194 project.of(ledger(world)) 195 196 # --------------------------------------------------------------------------- 197 # answers 198 199 # the key a question is looked up by 200 def key(ask: Ask) -> String: 201 match ask: 202 case Read{path}: 203 "read\n" ++ path 204 case Exec{path}: 205 "exec\n" ++ path 206 case Locate{path}: 207 "locate\n" ++ path 208 209 # an answer looked up, or none yet 210 type Heard is Data: 211 Heard{answer: Answer} 212 Open{} 213 214 # this answer when it is the question's, otherwise the rest's 215 def look.step(hit: Bool, answer: Answer, rest: Unit -> Heard) -> Heard: 216 match hit: 217 case True{}: 218 Heard{answer} 219 case False{}: 220 rest(Unit{}) 221 222 # the first answer given to a question 223 def look(rs: List<&2, Reply>, +want: String) -> Heard: 224 match rs: 225 case []: 226 Open{} 227 case Reply{ask, answer} <> t: 228 look.step(String.eq(key(ask), want), answer, _u => look(t, want)) 229 230 # the answer to a question, or none yet 231 def heard(+world: World, ask: Ask) -> Heard: 232 look(replies(world), key(ask))