Module: Everywhere::Shellout
- Defined in:
- lib/everywhere/shellout.rb
Overview
All external commands (tebako, cargo, the app's own bin/dev) run outside this gem's bundle — they have their own gem environments.
Class Method Summary collapse
-
.capture(*cmd) ⇒ Object
Run a command and capture its combined stdout+stderr.
-
.child_env(overrides = {}) ⇒ Object
The environment a child should get: this gem's bundle stripped back out, plus the caller's overrides.
-
.pty? ⇒ Boolean
PTY is POSIX-only.
- .run!(env, *cmd, chdir: Dir.pwd) ⇒ Object
-
.run?(*cmd, chdir: Dir.pwd, quiet: false) ⇒ Boolean
quiet also detaches stdin.
-
.run_logged!(env, cmd, log:, filter: nil) ⇒ Object
Run a command, capturing its full combined output to a log file while showing a (optionally filtered) view on the console.
-
.spawn(env, cmd, chdir:) ⇒ Object
pgroup: the child leads its own process group so teardown can TERM the whole tree (foreman + watchers) without signalling the CLI itself.
-
.spawn_pty(env, cmd, chdir:) ⇒ Object
Same, but on a pseudo-terminal, returning [master_io, pid].
-
.unbundled_base ⇒ Object
Bundler.unbundled_env derives from
original_env, a snapshot frozen at load time, so this is genuinely constant for the life of the process.
Class Method Details
.capture(*cmd) ⇒ Object
Run a command and capture its combined stdout+stderr. Returns [output_string, Process::Status]. For probing tool output (codesign, stapler, notarytool) rather than driving a build. A leading Hash is env overrides, matching Open3's own convention.
60 61 62 63 64 65 66 |
# File 'lib/everywhere/shellout.rb', line 60 def capture(*cmd) require "open3" overrides = cmd.first.is_a?(Hash) ? cmd.shift : {} Open3.capture2e(child_env(overrides), *cmd) rescue Errno::ENOENT ["", nil] end |
.child_env(overrides = {}) ⇒ Object
The environment a child should get: this gem's bundle stripped back out, plus the caller's overrides.
This is handed to spawn as its env argument rather than swapped into ENV,
which is what Bundler.with_unbundled_env does (ENV.replace, twice, around
the block). That is process-global mutable state, and every dev now runs
the iOS and Android builds concurrently: two overlapping swaps race so that
one child inherits the CLI's bundle after all — the exact thing unbundling
exists to prevent — and ENV is left wrong for the rest of the session.
A nil value tells spawn to REMOVE that key, which is most of what unbundling is. The resulting child environment is identical to what with_unbundled_env produced, since Bundler.unbundled_env is derived from the snapshot taken before Bundler was activated either way.
27 28 29 30 31 32 33 |
# File 'lib/everywhere/shellout.rb', line 27 def child_env(overrides = {}) base = unbundled_base delta = {} ENV.each_key { |key| delta[key] = nil unless base.key?(key) } base.each { |key, value| delta[key] = value unless ENV[key] == value } delta.merge(overrides.transform_keys(&:to_s)) end |
.pty? ⇒ Boolean
PTY is POSIX-only. Callers fall back to spawn + inherited fds.
149 150 151 152 153 154 |
# File 'lib/everywhere/shellout.rb', line 149 def pty? require "pty" true rescue LoadError false end |
.run!(env, *cmd, chdir: Dir.pwd) ⇒ Object
41 42 43 44 |
# File 'lib/everywhere/shellout.rb', line 41 def run!(env, *cmd, chdir: Dir.pwd) success = system(child_env(env), *cmd, chdir: chdir) UI.die!("command failed: #{cmd.join(" ")}") unless success end |
.run?(*cmd, chdir: Dir.pwd, quiet: false) ⇒ Boolean
quiet also detaches stdin. A quiet command is by definition one that has
nothing to say to the terminal, and every dev reads single keystrokes
from that same terminal — a child left holding stdin silently eats the
keypresses the menu is waiting on.
50 51 52 53 54 |
# File 'lib/everywhere/shellout.rb', line 50 def run?(*cmd, chdir: Dir.pwd, quiet: false) opts = { chdir: chdir } opts.update(in: File::NULL, out: File::NULL, err: File::NULL) if quiet system(child_env, *cmd, **opts) end |
.run_logged!(env, cmd, log:, filter: nil) ⇒ Object
Run a command, capturing its full combined output to a log file while showing a (optionally filtered) view on the console.
Pass filter: a LogFilter (or any #call(line) -> String | :drop | nil) to prettify what a human sees; the on-disk log always gets the raw stream, so nothing is lost. Unrecognized lines (filter returns nil) are shown dimmed and indented so they read as background detail under the current step.
The signature is deliberately unchanged: the builder tests stub this with fixed-signature lambdas, and cancellation is wired up out of band through ChildProcesses instead.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/everywhere/shellout.rb', line 79 def run_logged!(env, cmd, log:, filter: nil) require "open3" success = false File.open(log, "w") do |logf| # Inside an `every dev` worker the build leads its own process group, # for two reasons: a Ctrl-C at the terminal no longer reaches # xcodebuild/gradlew behind the CLI's back (the CLI decides when a build # dies), and teardown can TERM the whole compile tree with one signal. # Outside dev this stays off, so Ctrl-C during `every build` still stops # the compiler as it always has. opts = ChildProcesses.isolated? ? { pgroup: true } : {} Open3.popen2e(child_env(env), *cmd, **opts) do |stdin, out, wait| stdin.close ChildProcesses.track(wait.pid) begin out.each_line do |raw| logf.write(raw) emit(raw, filter) end success = wait.value.success? ensure ChildProcesses.untrack(wait.pid) end end end UI.die!("command failed: #{cmd.join(" ")} (full log: #{log})") unless success end |
.spawn(env, cmd, chdir:) ⇒ Object
pgroup: the child leads its own process group so teardown can TERM the whole tree (foreman + watchers) without signalling the CLI itself.
123 124 125 |
# File 'lib/everywhere/shellout.rb', line 123 def spawn(env, cmd, chdir:) Process.spawn(child_env(env), cmd, chdir: chdir, pgroup: true) end |
.spawn_pty(env, cmd, chdir:) ⇒ Object
Same, but on a pseudo-terminal, returning [master_io, pid].
every dev relays the dev server's and desktop shell's output itself so
it can tag each line with its source and keep the dock pinned at the
bottom. A plain pipe would work, but the children would see a non-TTY and
foreman, cargo and friends would drop their colors and progress output; on
a pty they behave exactly as they do when you run them by hand.
PTY.spawn calls setsid, so the child leads its own session: a terminal SIGINT can't reach it, and Process.kill("TERM", -pid) still takes down the whole tree.
138 139 140 141 142 143 144 145 146 |
# File 'lib/everywhere/shellout.rb', line 138 def spawn_pty(env, cmd, chdir:) require "pty" # PTY.spawn hands back the read and write ends of the pty MASTER. We never # forward stdin, so the write end is closed immediately — the child keeps # running because the read end still holds the master open. reader, writer, pid = PTY.spawn(child_env(env), cmd, chdir: chdir) writer.close [reader, pid] end |
.unbundled_base ⇒ Object
Bundler.unbundled_env derives from original_env, a snapshot frozen at
load time, so this is genuinely constant for the life of the process.
37 38 39 |
# File 'lib/everywhere/shellout.rb', line 37 def unbundled_base @unbundled_base ||= defined?(Bundler) ? Bundler.unbundled_env : ENV.to_h end |