1 # share/say: the words a long step says, and a spinner for the wait. 2 # 3 # A clone, a hub fetch or a native build can sit there for half a minute, and 4 # a step that prints nothing looks stopped. The line is printed before the 5 # work. On a terminal it is drawn again in place, with a mark that moves, 6 # until the step finishes; the mark is a child, because the work itself is 7 # one program waited on to the end and nothing can be printed while it runs. 8 # That child is started with its own session and its standard streams closed, 9 # so it opens the terminal by the path this process's own stderr or stdout 10 # already has. A pipe is not a terminal: the line is printed once and left, 11 # which is what a log wants. 12 # 13 # The words and the scripts the mark runs are here; what starts and stops the 14 # mark is share/spin, which runs programs and so reaches foreign code a law 15 # may not import. 16 import Base 17 import ../pkg/path.bend as P 18 19 # a spinner that is running, or none. An empty pid means the line was already 20 # printed and nothing has to be stopped. `msg` is what the terminal is owed 21 # when the mark is cleared. 22 type Spin is Data: 23 Spin{dev: String, pid: String, msg: String} 24 25 # the first twelve characters of a commit, which is enough to tell one from 26 # the next. A shorter name is already short. 27 def brief.go(short: Bool, +rev: String) -> String: 28 match short: 29 case True{}: 30 rev 31 case False{}: 32 String.take(rev, 12n) 33 34 # a commit as a line shows it 35 def brief(+rev: String) -> String: 36 brief.go(Nat.is_le(String.length(rev), 12n), rev) 37 38 # a package's name when its entry has one, otherwise the hash. `main` is kept: 39 # it is the name the file has. 40 def label.go(empty: Bool, +hash: String, +name: String) -> String: 41 match empty: 42 case True{}: 43 hash 44 case False{}: 45 name 46 47 # what a line calls a package 48 def label(+hash: String, +entry: String) -> String: 49 +name = P.unbend(P.base(entry)) 50 label.go(String.is_empty(name), hash, name) 51 52 # `dep sha256: git fetch (https://github.com/…)` 53 def dep.git(+name: String, +url: String) -> String: 54 "dep " ++ name ++ ": git fetch (" ++ url ++ ")" 55 56 # a package taken from the hub 57 def dep.hub(+name: String) -> String: 58 "dep " ++ name ++ ": hub fetch" 59 60 # a binary already built for this commit 61 def reused(+name: String) -> String: 62 name ++ ": cached" 63 64 # the lock resolving every dependency 65 def resolving() -> String: 66 "resolving deps" 67 68 def resolving.at(+name: String) -> String: 69 "resolving " ++ name 70 71 def git.fetch(+url: String) -> String: 72 "git fetch (" ++ url ++ ")" 73 74 # a binary being built 75 def building(+name: String) -> String: 76 "building " ++ name 77 78 # a binary being linked into place 79 def linking(+at: String) -> String: 80 "linking " ++ at 81 82 # a binary built and linked 83 def installed(+name: String, +at: String) -> String: 84 "installed " ++ name ++ " -> " ++ at 85 86 # a binary linked, with nothing to build 87 def linked(+name: String, +at: String) -> String: 88 "linked " ++ name ++ " -> " ++ at 89 90 # a commit's tree being weighed for its NAR hash 91 def hashing(+rev: String) -> String: 92 "hashing " ++ brief(rev) 93 94 # a device path that is a terminal. A pipe and `/dev/null` are not. 95 def tty.at(+path: String) -> Bool: 96 Bool.or(String.starts_with(path, "/dev/pts/"), 97 Bool.or(String.eq(path, "/dev/console"), String.starts_with(path, "/dev/tty"))) 98 99 def tty.out(on: Bool, +out: String) -> String: 100 match on: 101 case True{}: 102 out 103 case False{}: 104 "" 105 106 # stderr's terminal when it is one, otherwise stdout's, otherwise none 107 def tty.pick.go(on: Bool, +err: String, +out: String) -> String: 108 match on: 109 case True{}: 110 err 111 case False{}: 112 tty.out(tty.at(out), out) 113 114 def tty.pick(+err: String, +out: String) -> String: 115 tty.pick.go(tty.at(err), err, out) 116 117 # the hub answered 404 for a manifest. A missing file is a different miss: 118 # its path is not `/manifest`. 119 def hub.miss(+why: String) -> Bool: 120 Bool.and(String.contains(why, "/manifest:"), 121 String.ends_with(why, "the server answered 404")) 122 123 def hub.explain.go(miss: Bool, +hash: String, +url: String, +why: String) -> String: 124 match miss: 125 case True{}: 126 "ez: " ++ hash ++ ": the hub has no manifest for this package (" ++ url ++ 127 " answered 404). Re-lock with `ez lock`, fix the dependency, or vendor it offline (`ez add`)." 128 case False{}: 129 "ez: " ++ why 130 131 def hub.explain(+hash: String, +url: String, +why: String) -> String: 132 hub.explain.go(hub.miss(why), hash, url, why) 133 134 # the commit the words name. A tag is what a person pinned; a bare rev is not. 135 def drift.where(bare: Bool, +tag: String, +rev: String) -> String: 136 match bare: 137 case True{}: 138 "commit " ++ rev 139 case False{}: 140 "tagged commit " ++ tag ++ " (" ++ rev ++ ")" 141 142 # the tagged commit's tree no longer hashes to the pin. Upgrade will not 143 # invent a new hash for that: the pin is what the ledger says, and a drift 144 # is the ledger and the tree disagreeing. 145 def drift(+name: String, +tag: String, +rev: String, +old: String, +new: String) -> String: 146 "ez: " ++ name ++ ": the tree hash of " ++ 147 drift.where(String.is_empty(tag), tag, rev) ++ 148 " no longer matches the pin (the package moved, or the hash is wrong: " ++ 149 old ++ " on the pin, " ++ new ++ 150 " in the tree). `ez lock --upgrade` cannot fix a drifted pin. Update the dependency hash, re-add the package, or check the tag." 151 152 # a drift judged before the tree was weighed, so there are not two hashes to show 153 def stuck(+name: String) -> String: 154 "ez: " ++ name ++ 155 ": the pin drifted. `ez lock --upgrade` cannot fix a drifted pin. Update the dependency hash, re-add the package, or check the tag." 156 157 # a `[tools.` section in the ledger text. The parser is not asked: a ledger 158 # that does not parse can still have named the command. 159 def has.tools(+text: String) -> Bool: 160 String.contains(text, "[tools.") 161 162 # `ez sync` is not a command. A ledger that pins tools already has the one 163 # that builds them; a ledger that does not is pointed at lock and install. 164 def hint(tools: Bool) -> String: 165 match tools: 166 case True{}: 167 "hint: `sync` is not a command; use `ez tool sync`" 168 case False{}: 169 "hint: `sync` is not a command. Use `ez lock` to resolve dependencies, or `ez tool install ` to link a binary." 170 171 # the parent's fd, by number. The child cannot look at its own: that one was 172 # replaced with a pipe before this ran. `$1` is the number. 173 def probe() -> String: 174 "ppid=; while read -r k v; do case $k in PPid:) ppid=$v;; esac; done < /proc/self/status; readlink /proc/$ppid/fd/$1 2>/dev/null" 175 176 # a mark on the terminal while the parent is alive. `$1` is the terminal, 177 # `$2` the line. The mark walks ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏. An empty parent 178 # id exits rather than spinning on `/proc`. 179 def frames() -> String: 180 "ppid=; while read -r k v; do case $k in PPid:) ppid=$v;; esac; done < /proc/self/status; [ -n \"$ppid\" ] || exit 0; while [ -d /proc/$ppid ]; do printf '\\r%s %s' '⠋' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠙' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠹' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠸' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠼' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠴' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠦' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠧' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠇' \"$2\" >\"$1\"; sleep 0.08; [ -d /proc/$ppid ] || break; printf '\\r%s %s' '⠏' \"$2\" >\"$1\"; sleep 0.08; done" 181 182 # the mark wiped, and the line left in its place 183 def settle() -> String: 184 "printf '\\r\\033[K%s\\n' \"$2\" >\"$1\""