ruby_everywhere 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/every +2 -13
- data/exe/rbe +2 -13
- data/lib/everywhere/boot.rb +9 -2
- data/lib/everywhere/builders/android.rb +16 -42
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +7 -23
- data/lib/everywhere/builders/ios.rb +49 -52
- data/lib/everywhere/builders/native_sources.rb +38 -0
- data/lib/everywhere/child_processes.rb +4 -4
- data/lib/everywhere/child_supervision.rb +172 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +20 -9
- data/lib/everywhere/commands/dev.rb +59 -284
- data/lib/everywhere/commands/doctor.rb +6 -6
- data/lib/everywhere/commands/install.rb +1 -0
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +5 -6
- data/lib/everywhere/commands/platform/login.rb +4 -4
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +62 -7
- data/lib/everywhere/commands/preview.rb +24 -97
- data/lib/everywhere/commands/publish.rb +2 -0
- data/lib/everywhere/commands/release.rb +14 -11
- data/lib/everywhere/commands/shell_dir.rb +2 -0
- data/lib/everywhere/commands/updates_keygen.rb +25 -1
- data/lib/everywhere/config/app.rb +126 -0
- data/lib/everywhere/config/auth.rb +108 -0
- data/lib/everywhere/config/data.rb +50 -0
- data/lib/everywhere/config/deep_linking.rb +107 -0
- data/lib/everywhere/config/desktop_ui.rb +153 -0
- data/lib/everywhere/config/mobile.rb +211 -0
- data/lib/everywhere/config/native_desktop.rb +168 -0
- data/lib/everywhere/config/native_mobile.rb +337 -0
- data/lib/everywhere/config/shell.rb +57 -0
- data/lib/everywhere/config/updates.rb +63 -0
- data/lib/everywhere/config.rb +30 -1423
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +2 -2
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +11 -5
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +23 -7
- data/lib/everywhere/platform/client.rb +19 -1
- data/lib/everywhere/platform/snapshot.rb +6 -4
- data/lib/everywhere/plist.rb +17 -0
- data/lib/everywhere/png.rb +1 -0
- data/lib/everywhere/raw_tty.rb +51 -0
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/tab_filter.rb +36 -0
- data/lib/everywhere/task_pool.rb +3 -4
- data/lib/everywhere/ui.rb +6 -1
- data/lib/everywhere/version.rb +6 -4
- data/lib/everywhere.rb +1 -2
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +0 -2
- data/support/release/macos/notarize.sh +3 -0
- metadata +24 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require_relative "clock"
|
|
5
|
+
require_relative "child_processes"
|
|
6
|
+
require_relative "relay"
|
|
7
|
+
require_relative "shellout"
|
|
8
|
+
require_relative "ui"
|
|
9
|
+
|
|
10
|
+
module Everywhere
|
|
11
|
+
# The long-lived children a session keeps on a leash, one per key: `every
|
|
12
|
+
# dev`'s dev server, desktop shell and log stream; `every preview`'s server
|
|
13
|
+
# and tunnel.
|
|
14
|
+
#
|
|
15
|
+
# It sits above ChildProcesses — which knows how to signal one pid's process
|
|
16
|
+
# group but nothing about names — and beside Shellout, which knows how to
|
|
17
|
+
# spawn one child but never remembers it. What lives here is the
|
|
18
|
+
# session-scoped {key => pid} registry, and the teardown that must not let a
|
|
19
|
+
# child outlive the CLI that opened it.
|
|
20
|
+
#
|
|
21
|
+
# A mixin rather than a collaborator object: the registry has exactly one
|
|
22
|
+
# owner (the command), and these read as the command's own vocabulary —
|
|
23
|
+
# stop_child(:web), child_pid(:desktop).
|
|
24
|
+
module ChildSupervision
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Children are spawned from pool threads as well as the main one.
|
|
28
|
+
def init_children!
|
|
29
|
+
@pids = {}
|
|
30
|
+
@relays = {}
|
|
31
|
+
@children = Mutex.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Spawn a long-lived child on a pty and relay its output, one tagged line
|
|
35
|
+
# at a time. A pty rather than a pipe so foreman, cargo and friends still
|
|
36
|
+
# believe they're on a terminal and keep their colors; PTY.spawn also
|
|
37
|
+
# setsids, so a Ctrl-C at our terminal can't reach them and teardown
|
|
38
|
+
# decides when they die.
|
|
39
|
+
def supervise(key, env, cmd, chdir:, filter: nil, log: nil, on_line: nil)
|
|
40
|
+
io, pid = spawn_child(env, cmd, chdir: chdir)
|
|
41
|
+
thread = relay_thread(key, io, pid, filter: filter, log: log, on_line: on_line) if io
|
|
42
|
+
Process.detach(pid)
|
|
43
|
+
@children.synchronize do
|
|
44
|
+
@pids[key] = pid
|
|
45
|
+
@relays[key] = thread if thread
|
|
46
|
+
end
|
|
47
|
+
pid
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# How this command puts a child into the world: [io, pid], where io is the
|
|
51
|
+
# side we read its output from — or nil for a child that was handed our own
|
|
52
|
+
# fds and prints for itself, which gets no relay. Overridden per command,
|
|
53
|
+
# because only the pty path is shared: what to do when there is no pty is
|
|
54
|
+
# exactly where dev and preview differ.
|
|
55
|
+
def spawn_child(env, cmd, chdir:)
|
|
56
|
+
Shellout.spawn_pty(env, cmd, chdir: chdir)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def relay_thread(key, io, pid, filter:, log:, on_line:)
|
|
60
|
+
Thread.new do
|
|
61
|
+
Thread.current.name = "relay-#{key}"
|
|
62
|
+
Thread.current.report_on_exception = false
|
|
63
|
+
begin
|
|
64
|
+
Relay.new(io, source: key, log: log, filter: filter, on_line: on_line).run
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
# Losing a relay costs us that child's output, nothing more. It must
|
|
67
|
+
# not surface later out of the join in teardown.
|
|
68
|
+
Kernel.warn("#{key} output relay stopped: #{e.class}: #{e.message}")
|
|
69
|
+
end
|
|
70
|
+
on_child_exit(key, pid)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The child's output ended, which for a supervised child means it died.
|
|
75
|
+
# Nothing to do by default — the pid is still in the registry either way,
|
|
76
|
+
# and teardown reaps it.
|
|
77
|
+
def on_child_exit(key, pid); end
|
|
78
|
+
|
|
79
|
+
# TERM the child's whole process group, escalate to KILL, and wait for the
|
|
80
|
+
# relay to drain so its last lines aren't lost. The pid is dropped from the
|
|
81
|
+
# registry FIRST, so the relay knows this death was asked for.
|
|
82
|
+
def stop_child(key, grace: 5)
|
|
83
|
+
pid, thread = @children.synchronize { [@pids.delete(key), @relays.delete(key)] }
|
|
84
|
+
return unless pid
|
|
85
|
+
|
|
86
|
+
reap([pid], grace: grace)
|
|
87
|
+
thread&.join(1)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# TERM each process group, wait for them together rather than serially,
|
|
91
|
+
# then KILL the stragglers.
|
|
92
|
+
def reap(pids, grace: 5)
|
|
93
|
+
live = pids.compact.select { |pid| ChildProcesses.alive?(pid) }
|
|
94
|
+
return if live.empty?
|
|
95
|
+
|
|
96
|
+
live.each { |pid| ChildProcesses.signal(pid, "TERM") }
|
|
97
|
+
deadline = Clock.monotonic + grace
|
|
98
|
+
sleep(0.1) while live.any? { |pid| ChildProcesses.alive?(pid) } && Clock.monotonic < deadline
|
|
99
|
+
live.select { |pid| ChildProcesses.alive?(pid) }.each { |pid| ChildProcesses.signal(pid, "KILL") }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def child_pid(key) = @children.synchronize { @pids[key] }
|
|
103
|
+
|
|
104
|
+
# Teardown must never raise — it runs from an ensure, where an exception
|
|
105
|
+
# would mask whatever actually ended the session — and one failed step
|
|
106
|
+
# must not skip the rest, or a child survives the CLI that spawned it.
|
|
107
|
+
# That ensure can also run before init_children! ever did, which is why the
|
|
108
|
+
# registry is read through a nil guard.
|
|
109
|
+
#
|
|
110
|
+
# `before` is whatever the host has to shut down ahead of its children —
|
|
111
|
+
# for `every dev`, the dock's hold on the terminal and the build pool.
|
|
112
|
+
def teardown_children(grace: 2, before: [])
|
|
113
|
+
@stopping = true
|
|
114
|
+
pids, relays = @children ? @children.synchronize { [@pids.values, @relays.values] } : [[], []]
|
|
115
|
+
(before + [
|
|
116
|
+
# TERM everything, then give it a moment and KILL whatever is left.
|
|
117
|
+
# The CLI exits the instant this returns, so anything still alive is
|
|
118
|
+
# orphaned — and a windowed shell is exactly the kind of child that
|
|
119
|
+
# doesn't drop dead on the first signal.
|
|
120
|
+
-> { reap(pids, grace: grace) },
|
|
121
|
+
-> { relays.each { |thread| thread.join(1) } }
|
|
122
|
+
]).each do |step|
|
|
123
|
+
step.call
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
Kernel.warn("teardown: #{e.class}: #{e.message}")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Checked once, up front: Integer() would otherwise raise from inside
|
|
130
|
+
# port_open? — an ArgumentError backtrace after the dock has already
|
|
131
|
+
# taken over the terminal.
|
|
132
|
+
def validate_port!(port)
|
|
133
|
+
number = begin
|
|
134
|
+
Integer(port.to_s, 10)
|
|
135
|
+
rescue ArgumentError, TypeError
|
|
136
|
+
UI.die!("--port must be a number (got #{port.inspect})")
|
|
137
|
+
end
|
|
138
|
+
UI.die!("--port must be between 1 and 65535 (got #{number})") unless (1..65_535).cover?(number)
|
|
139
|
+
number.to_s
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Any connect error means "nothing listening yet" for our purposes —
|
|
143
|
+
# ECONNREFUSED is the common one, but a firewall or an exhausted fd table
|
|
144
|
+
# answers differently and must not abort the session.
|
|
145
|
+
def port_open?(port)
|
|
146
|
+
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
147
|
+
true
|
|
148
|
+
rescue SystemCallError
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Watches the child as well as the port: a dev server that dies on
|
|
153
|
+
# startup (no `bundle install`, a bin/dev whose foreman isn't installed)
|
|
154
|
+
# never opens the port, and waiting out the full timeout for it buries
|
|
155
|
+
# the error it already printed under a minute of silence.
|
|
156
|
+
#
|
|
157
|
+
# The hints are appended to the message as-is, for a caller that can say
|
|
158
|
+
# something more specific about why its server never came up.
|
|
159
|
+
def wait_for_port(port, pid: nil, timeout: 60, dead_hint: nil, slow_hint: nil)
|
|
160
|
+
deadline = Clock.monotonic + timeout
|
|
161
|
+
until port_open?(port)
|
|
162
|
+
if pid && !ChildProcesses.alive?(pid)
|
|
163
|
+
UI.die!("the dev server exited before it finished booting — scroll up for its output#{dead_hint}")
|
|
164
|
+
end
|
|
165
|
+
if Clock.monotonic > deadline
|
|
166
|
+
UI.die!("dev server never came up on port #{port} after #{timeout}s#{slow_hint}")
|
|
167
|
+
end
|
|
168
|
+
sleep 0.25
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# The one clock every deadline in the CLI measures against. Monotonic, so a
|
|
5
|
+
# timeout can't be lengthened or skipped by an NTP step or a DST change
|
|
6
|
+
# mid-build.
|
|
7
|
+
module Clock
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -5,10 +5,12 @@ require "fileutils"
|
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require "shellwords"
|
|
7
7
|
require_relative "../shellout"
|
|
8
|
+
require_relative "../host"
|
|
8
9
|
require_relative "../log_filter"
|
|
9
10
|
require_relative "../png"
|
|
10
11
|
require_relative "../icon"
|
|
11
12
|
require_relative "../paths"
|
|
13
|
+
require_relative "../plist"
|
|
12
14
|
require_relative "../desktop_assets"
|
|
13
15
|
require_relative "../builders/ios"
|
|
14
16
|
require_relative "../builders/android"
|
|
@@ -63,6 +65,17 @@ module Everywhere
|
|
|
63
65
|
root_path = File.expand_path(root)
|
|
64
66
|
config = Everywhere::Config.load(root_path)
|
|
65
67
|
|
|
68
|
+
# Before any builder runs: the bundle id names the work dirs they wipe
|
|
69
|
+
# and recreate, so a malformed one is a wrong path, not a wrong label.
|
|
70
|
+
identity_errors = config.identity_errors
|
|
71
|
+
UI.die!("app: in config/everywhere.yml:\n#{identity_errors.join("\n")}") unless identity_errors.empty?
|
|
72
|
+
|
|
73
|
+
# Baked into the shell here, so this is the last moment an unusable
|
|
74
|
+
# feed URL can still be a message instead of a shipped app that never
|
|
75
|
+
# updates.
|
|
76
|
+
updates_errors = config.updates_errors
|
|
77
|
+
UI.die!("updates: in config/everywhere.yml:\n#{updates_errors.join("\n")}") unless updates_errors.empty?
|
|
78
|
+
|
|
66
79
|
# Builder dispatch on the target's os (build-engine.md §5): macOS is
|
|
67
80
|
# the tebako+tauri path below; the mobile targets are Hotwire Native
|
|
68
81
|
# shells, each built by its own builder. Anything else has no builder yet.
|
|
@@ -75,13 +88,13 @@ module Everywhere
|
|
|
75
88
|
app = mobile_builder(target).new(config: config, root: root_path, target: target,
|
|
76
89
|
template_dir: template_dir, **channel_option(target, channel))
|
|
77
90
|
.build!(dist_dir: dist_dir, **mobile_options(target, device: device, format: format))
|
|
78
|
-
UI.success("built #{
|
|
91
|
+
UI.success("built #{UI.rel_path(app, root_path)}")
|
|
79
92
|
return
|
|
80
93
|
when "macos" # ...which is the rest of this method
|
|
81
94
|
# Every step of it ends in codesign and an .app bundle, so a Linux
|
|
82
95
|
# host can only fail — the question is whether it fails here or
|
|
83
96
|
# thirty minutes in, at the first codesign.
|
|
84
|
-
UI.die!("desktop builds are macOS-only for now") unless
|
|
97
|
+
UI.die!("desktop builds are macOS-only for now") unless Host.darwin?
|
|
85
98
|
else
|
|
86
99
|
UI.die!("no builder for target #{target} — macos-*, ios-* and android-* are supported")
|
|
87
100
|
end
|
|
@@ -124,15 +137,15 @@ module Everywhere
|
|
|
124
137
|
UI.step("#{UI.bold(framework.name)} app at #{framework.root}")
|
|
125
138
|
|
|
126
139
|
if skip_press
|
|
127
|
-
UI.die!("--skip-press given but #{
|
|
128
|
-
UI.step("reusing pressed binary #{
|
|
140
|
+
UI.die!("--skip-press given but #{UI.rel_path(output_path, root_path)} doesn't exist") unless File.exist?(output_path)
|
|
141
|
+
UI.step("reusing pressed binary #{UI.rel_path(output_path, root_path)}")
|
|
129
142
|
else
|
|
130
143
|
press!(framework, entry, ruby, output_path)
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
bundle_app(output_path, app_name, shell_dir, config, target: target)
|
|
134
147
|
|
|
135
|
-
UI.success("built #{
|
|
148
|
+
UI.success("built #{UI.rel_path(output_path, root_path)} (#{(File.size(output_path) / 1024.0 / 1024.0).round}MB)")
|
|
136
149
|
end
|
|
137
150
|
|
|
138
151
|
private
|
|
@@ -193,8 +206,6 @@ module Everywhere
|
|
|
193
206
|
target
|
|
194
207
|
end
|
|
195
208
|
|
|
196
|
-
def rel(path, root_path) = UI.short_path(path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", ""))
|
|
197
|
-
|
|
198
209
|
# The precompile step runs against the HOST Ruby — unlike tebako press,
|
|
199
210
|
# which installs the app's gems itself inside the packaged Ruby. A fresh
|
|
200
211
|
# machine (a hosted build runner, a brand-new checkout) has Gemfile.lock
|
|
@@ -400,6 +411,8 @@ module Everywhere
|
|
|
400
411
|
# and CFBundleVersion come from `app.version` in everywhere.yml (via
|
|
401
412
|
# config.version), so the bundle and the release.json receipt agree.
|
|
402
413
|
def info_plist(app_name, exe_name, icon, identifier, version)
|
|
414
|
+
app_name, exe_name, icon, identifier, version =
|
|
415
|
+
[app_name, exe_name, icon, identifier, version].map { |v| Everywhere::Plist.escape(v) if v }
|
|
403
416
|
icon_keys = icon ? " <key>CFBundleIconFile</key><string>#{icon}</string>\n" : ""
|
|
404
417
|
<<~PLIST
|
|
405
418
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -420,8 +433,6 @@ module Everywhere
|
|
|
420
433
|
PLIST
|
|
421
434
|
end
|
|
422
435
|
|
|
423
|
-
private
|
|
424
|
-
|
|
425
436
|
# Tebako's output is thousands of lines; re-surface the warnings that
|
|
426
437
|
# actually matter to the app developer after the build finishes.
|
|
427
438
|
def surface_warnings(log)
|