ruby_everywhere 0.13.0 → 0.14.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/README.md +30 -30
- data/bridge/README.md +23 -23
- data/lib/everywhere/android_resources.rb +9 -9
- data/lib/everywhere/android_sdk.rb +9 -9
- data/lib/everywhere/asset_catalog.rb +7 -7
- data/lib/everywhere/blake2b.rb +1 -1
- data/lib/everywhere/boot.rb +1 -1
- data/lib/everywhere/builders/android.rb +25 -25
- data/lib/everywhere/builders/desktop.rb +6 -6
- data/lib/everywhere/builders/ios.rb +14 -14
- data/lib/everywhere/builders/native_sources.rb +1 -1
- data/lib/everywhere/child_supervision.rb +1 -1
- data/lib/everywhere/commands/build.rb +19 -19
- data/lib/everywhere/commands/clean.rb +1 -1
- data/lib/everywhere/commands/dev.rb +11 -11
- data/lib/everywhere/commands/doctor.rb +7 -7
- data/lib/everywhere/commands/icon.rb +2 -2
- data/lib/everywhere/commands/install.rb +21 -21
- data/lib/everywhere/commands/logs.rb +6 -6
- data/lib/everywhere/commands/platform/auth_status.rb +3 -3
- data/lib/everywhere/commands/platform/build.rb +7 -7
- data/lib/everywhere/commands/platform/login.rb +5 -5
- data/lib/everywhere/commands/platform/logout.rb +1 -1
- data/lib/everywhere/commands/platform/runner.rb +6 -6
- data/lib/everywhere/commands/preview.rb +14 -14
- data/lib/everywhere/commands/publish.rb +14 -14
- data/lib/everywhere/commands/release.rb +6 -6
- data/lib/everywhere/commands/updates_keygen.rb +4 -4
- data/lib/everywhere/config/app.rb +3 -3
- data/lib/everywhere/config/mobile.rb +2 -2
- data/lib/everywhere/config/native_desktop.rb +2 -2
- data/lib/everywhere/config/native_mobile.rb +3 -3
- data/lib/everywhere/config/updates.rb +2 -2
- data/lib/everywhere/database.rb +3 -3
- data/lib/everywhere/desktop_assets.rb +5 -5
- data/lib/everywhere/desktop_dev_app.rb +1 -1
- data/lib/everywhere/emulator.rb +5 -5
- data/lib/everywhere/log_filter.rb +1 -1
- data/lib/everywhere/omniauth.rb +1 -1
- data/lib/everywhere/paths.rb +1 -1
- data/lib/everywhere/platform/credentials.rb +1 -1
- data/lib/everywhere/request_context.rb +2 -2
- data/lib/everywhere/shellout.rb +2 -2
- data/lib/everywhere/simulator.rb +1 -1
- data/lib/everywhere/tab_filter.rb +2 -2
- data/lib/everywhere/version.rb +1 -1
- data/support/agents/AGENTS.md +7 -7
- data/support/agents/frameworks/hanami-views.md +8 -8
- data/support/agents/frameworks/hanami.md +2 -2
- data/support/agents/frameworks/rails-views.md +7 -7
- data/support/agents/frameworks/rails.md +8 -8
- data/support/agents/frameworks/sinatra-views.md +7 -7
- data/support/agents/frameworks/sinatra.md +2 -2
- data/support/agents/modes/local.md +2 -2
- data/support/agents/modes/remote.md +2 -2
- metadata +1 -1
|
@@ -65,7 +65,7 @@ module Everywhere
|
|
|
65
65
|
@url = lan_url(port)
|
|
66
66
|
else
|
|
67
67
|
Shellout.ensure_tool!("cloudflared",
|
|
68
|
-
"preview shares your app through a Cloudflare tunnel
|
|
68
|
+
"preview shares your app through a Cloudflare tunnel. " \
|
|
69
69
|
"install it with `brew install cloudflared` (or use --lan)")
|
|
70
70
|
validate_tunnel_options!(tunnel_name, tunnel_host)
|
|
71
71
|
@tunnel_name = tunnel_name
|
|
@@ -98,10 +98,10 @@ module Everywhere
|
|
|
98
98
|
rescue Everywhere::Error
|
|
99
99
|
config = Everywhere::Config.load(Dir.pwd)
|
|
100
100
|
if config.remote?
|
|
101
|
-
UI.die!("this app runs in remote mode
|
|
101
|
+
UI.die!("this app runs in remote mode; #{config.remote_url} is already online, share that " \
|
|
102
102
|
"(preview tunnels a local dev server)")
|
|
103
103
|
end
|
|
104
|
-
UI.die!("no supported framework here (Rails, Hanami, Sinatra)
|
|
104
|
+
UI.die!("no supported framework here (Rails, Hanami, Sinatra), so nothing to preview")
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def validate_tunnel_options!(name, host)
|
|
@@ -120,7 +120,7 @@ module Everywhere
|
|
|
120
120
|
|
|
121
121
|
def lan_ip
|
|
122
122
|
Socket.ip_address_list.find { |a| a.ipv4_private? }&.ip_address ||
|
|
123
|
-
UI.die!("couldn't find a LAN address
|
|
123
|
+
UI.die!("couldn't find a LAN address. Is this machine on a network? (drop --lan to tunnel instead)")
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def start_tunnel
|
|
@@ -146,7 +146,7 @@ module Everywhere
|
|
|
146
146
|
begin
|
|
147
147
|
Timeout.timeout(TUNNEL_BOOT_TIMEOUT) { found.pop }
|
|
148
148
|
rescue Timeout::Error
|
|
149
|
-
UI.die!("cloudflared never reported a tunnel URL after #{TUNNEL_BOOT_TIMEOUT}s
|
|
149
|
+
UI.die!("cloudflared never reported a tunnel URL after #{TUNNEL_BOOT_TIMEOUT}s. " \
|
|
150
150
|
"check your network, or share on the local network with --lan")
|
|
151
151
|
end
|
|
152
152
|
end
|
|
@@ -170,11 +170,11 @@ module Everywhere
|
|
|
170
170
|
service = rule&.fetch("service", nil).to_s
|
|
171
171
|
if rule.nil? || service.start_with?("http_status:")
|
|
172
172
|
UI.warn("your cloudflared config's ingress has no rule for #{@tunnel_host}, so requests will get " \
|
|
173
|
-
"the catch-all#{" (#{service})" unless service.empty?}
|
|
173
|
+
"the catch-all#{" (#{service})" unless service.empty?}. Add " \
|
|
174
174
|
"{hostname: #{@tunnel_host}, service: http://localhost:#{@port}} above it")
|
|
175
175
|
elsif (svc_port = service[/:(\d+)\z/, 1]) && Integer(svc_port) != @port
|
|
176
176
|
UI.warn("your cloudflared config's ingress routes #{@tunnel_host} to #{service}, but this preview " \
|
|
177
|
-
"runs on port #{@port}
|
|
177
|
+
"runs on port #{@port}. Point that rule at the preview, or match it with --port #{svc_port}")
|
|
178
178
|
end
|
|
179
179
|
end
|
|
180
180
|
|
|
@@ -220,7 +220,7 @@ module Everywhere
|
|
|
220
220
|
def warn_if_loopback_only
|
|
221
221
|
TCPSocket.new(URI.parse(@url).host, Integer(@port)).close
|
|
222
222
|
rescue SystemCallError
|
|
223
|
-
UI.warn("the dev server only listens on 127.0.0.1, so phones can't reach #{@url}
|
|
223
|
+
UI.warn("the dev server only listens on 127.0.0.1, so phones can't reach #{@url}. " \
|
|
224
224
|
"make bin/dev bind 0.0.0.0 (rackup --host 0.0.0.0), or drop --lan and tunnel instead")
|
|
225
225
|
end
|
|
226
226
|
|
|
@@ -231,7 +231,7 @@ module Everywhere
|
|
|
231
231
|
# here: that process has no EVERYWHERE_JUMP_TOKEN, so Jump could never
|
|
232
232
|
# pair with it.
|
|
233
233
|
if port_open?(port)
|
|
234
|
-
UI.die!("something is already running on port #{port}
|
|
234
|
+
UI.die!("something is already running on port #{port}, and preview must start the server itself " \
|
|
235
235
|
"(the Jump pairing token rides in on its environment); stop that server or use --port")
|
|
236
236
|
end
|
|
237
237
|
|
|
@@ -277,7 +277,7 @@ module Everywhere
|
|
|
277
277
|
UI.step("preview of #{UI.bold(@config.name)} is live → #{UI.cyan(@url)}")
|
|
278
278
|
show_qr
|
|
279
279
|
Console.print(" #{UI.dim("Manual entry in Jump:")} #{@url} #{UI.dim("token:")} #{@token}\n")
|
|
280
|
-
Console.print(UI.dim(" The link and QR die with this session
|
|
280
|
+
Console.print(UI.dim(" The link and QR die with this session. Ctrl-C to stop.\n"))
|
|
281
281
|
UI.note("waiting for devices…", marker: "⌁", color: :cyan)
|
|
282
282
|
@dock.show(footer_facts)
|
|
283
283
|
end
|
|
@@ -321,7 +321,7 @@ module Everywhere
|
|
|
321
321
|
end.join + "\e[0m"
|
|
322
322
|
end.join("\n") << "\e[0m\n"
|
|
323
323
|
rescue LoadError, StandardError => e
|
|
324
|
-
UI.warn("couldn't render the QR (#{e.class})
|
|
324
|
+
UI.warn("couldn't render the QR (#{e.class}); use the link below")
|
|
325
325
|
""
|
|
326
326
|
end
|
|
327
327
|
|
|
@@ -345,12 +345,12 @@ module Everywhere
|
|
|
345
345
|
break if pause == :quit
|
|
346
346
|
|
|
347
347
|
web = @children.synchronize { @pids[:web] }
|
|
348
|
-
UI.die!("the dev server exited
|
|
348
|
+
UI.die!("the dev server exited. Scroll up for its output") unless ChildProcesses.alive?(web)
|
|
349
349
|
|
|
350
350
|
tunnel = @children.synchronize { @pids[:tunnel] }
|
|
351
351
|
next if tunnel.nil? || ChildProcesses.alive?(tunnel)
|
|
352
352
|
|
|
353
|
-
UI.warn("the tunnel dropped
|
|
353
|
+
UI.warn("the tunnel dropped, starting a fresh one")
|
|
354
354
|
stop_child(:tunnel)
|
|
355
355
|
@url = start_tunnel
|
|
356
356
|
announce
|
|
@@ -384,7 +384,7 @@ module Everywhere
|
|
|
384
384
|
wait_for_port_close
|
|
385
385
|
@config = Everywhere::Config.load(@framework.root)
|
|
386
386
|
start_server(@port.to_s)
|
|
387
|
-
UI.note("server is back
|
|
387
|
+
UI.note("server is back, same URL, same token", marker: "⌁", color: :cyan)
|
|
388
388
|
end
|
|
389
389
|
|
|
390
390
|
# stop_child has reaped the process, but the kernel can hold its listener
|
|
@@ -60,7 +60,7 @@ module Everywhere
|
|
|
60
60
|
# RequestTimeExpired means the bucket rejected our SigV4 timestamp; the
|
|
61
61
|
# only realistic local cause is a wrong clock, and the raw message
|
|
62
62
|
# doesn't say so.
|
|
63
|
-
hint = e.message.include?("RequestTimeExpired") ? "
|
|
63
|
+
hint = e.message.include?("RequestTimeExpired") ? " (your system clock may be off by more than 15 minutes)" : ""
|
|
64
64
|
UI.die!("#{e.message}#{hint}")
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -106,12 +106,12 @@ module Everywhere
|
|
|
106
106
|
|
|
107
107
|
if (head = s3.head_object(key))
|
|
108
108
|
if head[:size] == size && !force
|
|
109
|
-
UI.step("#{filename} already published (same size)
|
|
109
|
+
UI.step("#{filename} already published (same size), skipping upload")
|
|
110
110
|
s3.put_object("#{key}.minisig", signature, content_type: "text/plain",
|
|
111
111
|
cache_control: UpdateManifest::IMMUTABLE_CACHE)
|
|
112
112
|
return
|
|
113
113
|
elsif !force
|
|
114
|
-
UI.die!("#{key} already exists with different content (#{head[:size]} vs #{size} bytes)
|
|
114
|
+
UI.die!("#{key} already exists with different content (#{head[:size]} vs #{size} bytes). " \
|
|
115
115
|
"bump app.version, or pass --force if you really mean to replace a published artifact")
|
|
116
116
|
end
|
|
117
117
|
end
|
|
@@ -157,32 +157,32 @@ module Everywhere
|
|
|
157
157
|
|
|
158
158
|
filename = File.basename(entry.fetch("url"))
|
|
159
159
|
key = UpdateManifest.artifact_key(filename, **loc)
|
|
160
|
-
UI.die!("#{key} is gone from the bucket
|
|
160
|
+
UI.die!("#{key} is gone from the bucket, so can't roll back to it") unless s3.head_object(key)
|
|
161
161
|
|
|
162
162
|
s3.put_object(UpdateManifest.latest_key(**loc), UpdateManifest.to_json(entry),
|
|
163
163
|
content_type: "application/json", cache_control: UpdateManifest::MUTABLE_CACHE)
|
|
164
164
|
maintain_alias!(filename, version, loc) if latest_alias
|
|
165
165
|
|
|
166
166
|
UI.ok("latest.json → #{version}")
|
|
167
|
-
UI.warn("apps still on newer #{entry["app"] ? entry.dig("app", "name") : "builds"} won't downgrade
|
|
167
|
+
UI.warn("apps still on newer #{entry["app"] ? entry.dig("app", "name") : "builds"} won't downgrade. " \
|
|
168
168
|
"ship a higher-versioned release to move everyone off the bad build")
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
# ---- pieces --------------------------------------------------------------
|
|
172
172
|
|
|
173
173
|
def preflight!
|
|
174
|
-
UI.die!("no updates.url in everywhere.yml
|
|
175
|
-
UI.die!("no updates.public_key in everywhere.yml
|
|
174
|
+
UI.die!("no updates.url in everywhere.yml. Add an updates: section (see `every updates keygen`)") unless @config.updates_url
|
|
175
|
+
UI.die!("no updates.public_key in everywhere.yml. Run `every updates keygen`") unless @config.updates_public_key
|
|
176
176
|
updates_errors = @config.updates_errors
|
|
177
177
|
UI.die!("updates: in config/everywhere.yml:\n#{updates_errors.join("\n")}") unless updates_errors.empty?
|
|
178
178
|
UI.die!("no updates.s3.bucket in everywhere.yml") if s3_config["bucket"].to_s.empty?
|
|
179
|
-
UI.die!("missing bucket credentials
|
|
179
|
+
UI.die!("missing bucket credentials. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY") if
|
|
180
180
|
ENV["AWS_ACCESS_KEY_ID"].to_s.empty? || ENV["AWS_SECRET_ACCESS_KEY"].to_s.empty?
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
def receipt_path(given)
|
|
184
184
|
path = given ? File.expand_path(given, @root) : File.join(@root, "dist", "release.json")
|
|
185
|
-
UI.die!("no receipt at #{UI.short_path(path)}
|
|
185
|
+
UI.die!("no receipt at #{UI.short_path(path)}. Run `every release` first") unless File.exist?(path)
|
|
186
186
|
path
|
|
187
187
|
end
|
|
188
188
|
|
|
@@ -193,7 +193,7 @@ module Everywhere
|
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
def verify_artifact!(receipt, receipt_path)
|
|
196
|
-
filename = receipt.dig("artifact", "filename") or UI.die!("receipt has no artifact record
|
|
196
|
+
filename = receipt.dig("artifact", "filename") or UI.die!("receipt has no artifact record. Re-run `every release`")
|
|
197
197
|
path = File.join(File.dirname(receipt_path), filename)
|
|
198
198
|
UI.die!("artifact #{UI.short_path(path)} not found next to the receipt") unless File.exist?(path)
|
|
199
199
|
|
|
@@ -219,14 +219,14 @@ module Everywhere
|
|
|
219
219
|
File.write("#{path}.minisig", signature)
|
|
220
220
|
Minisign.verify_file(path, signature: signature, public_key: @config.updates_public_key)
|
|
221
221
|
rescue Minisign::Error
|
|
222
|
-
UI.die!("signature doesn't verify against updates.public_key in everywhere.yml
|
|
222
|
+
UI.die!("signature doesn't verify against updates.public_key in everywhere.yml. " \
|
|
223
223
|
"the key you're signing with isn't the one shipped apps trust")
|
|
224
224
|
end
|
|
225
225
|
signature
|
|
226
226
|
rescue Minisign::PassphraseError
|
|
227
|
-
UI.die!("wrong or missing key passphrase
|
|
227
|
+
UI.die!("wrong or missing key passphrase. Set EVERYWHERE_SIGNING_KEY_PASSPHRASE")
|
|
228
228
|
rescue Minisign::Error => e
|
|
229
|
-
UI.die!("couldn't sign with that key
|
|
229
|
+
UI.die!("couldn't sign with that key: #{e.message}")
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
def signing_key(key_option)
|
|
@@ -241,7 +241,7 @@ module Everywhere
|
|
|
241
241
|
return ENV["EVERYWHERE_SIGNING_KEY"] if ENV["EVERYWHERE_SIGNING_KEY"]
|
|
242
242
|
|
|
243
243
|
default = File.join(Paths.cache_dir, "keys", "#{@config.bundle_id}.key")
|
|
244
|
-
UI.die!("no signing key found
|
|
244
|
+
UI.die!("no signing key found. Run `every updates keygen` (looked for #{UI.short_path(default)})") unless File.exist?(default)
|
|
245
245
|
File.read(default)
|
|
246
246
|
end
|
|
247
247
|
|
|
@@ -88,11 +88,11 @@ module Everywhere
|
|
|
88
88
|
UI.die!("`every release --target #{target}` runs on macOS only") unless Host.darwin?
|
|
89
89
|
UI.die!("signing engine missing at #{NOTARIZE}") unless File.exist?(NOTARIZE)
|
|
90
90
|
when "ios"
|
|
91
|
-
UI.die!("iOS releases need Xcode
|
|
91
|
+
UI.die!("iOS releases need Xcode; `every release --target #{target}` runs on macOS only") unless Host.darwin?
|
|
92
92
|
when "android"
|
|
93
93
|
nil # Gradle signs and builds anywhere the Android SDK does — Linux included.
|
|
94
94
|
else
|
|
95
|
-
UI.die!("no release builder for #{target}
|
|
95
|
+
UI.die!("no release builder for #{target}: macos-*, ios-* and android-* are supported")
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
# After the os check: the channel table is keyed by os, so an unknown
|
|
@@ -116,7 +116,7 @@ module Everywhere
|
|
|
116
116
|
# then read the signing facts back off the bundle.
|
|
117
117
|
def notarize!(dist_dir, root_path, env_file)
|
|
118
118
|
app_dir = Dir[File.join(dist_dir, "*.app")].max_by { |d| File.mtime(d) }
|
|
119
|
-
UI.die!("no .app found in #{dist_dir}
|
|
119
|
+
UI.die!("no .app found in #{dist_dir}. Run without --skip-build first") unless app_dir
|
|
120
120
|
|
|
121
121
|
env_file ||= discover_env_file(root_path)
|
|
122
122
|
env = env_file ? { "ENV_FILE" => env_file } : {}
|
|
@@ -126,7 +126,7 @@ module Everywhere
|
|
|
126
126
|
# with, and "which file did that come from" is the first thing you ask
|
|
127
127
|
# when a build signs as the wrong team.
|
|
128
128
|
UI.detail("signing env: #{UI.short_path(File.expand_path(env_file))}") if env_file
|
|
129
|
-
UI.warn("no signing env found
|
|
129
|
+
UI.warn("no signing env found. Set APPLE_* or pass --env-file (notarize.sh will fail without it)") unless env_file || ENV["APPLE_SIGNING_IDENTITY"]
|
|
130
130
|
log = "#{app_dir}.notarize.log"
|
|
131
131
|
Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log, filter: LogFilter.for(:notarize))
|
|
132
132
|
|
|
@@ -143,7 +143,7 @@ module Everywhere
|
|
|
143
143
|
found = patterns.filter_map { |p| Dir[File.join(dist_dir, p)].max_by { |f| File.mtime(f) } }.first
|
|
144
144
|
return found if found
|
|
145
145
|
|
|
146
|
-
UI.die!("no #{patterns.map { |p| p.delete("*") }.join(" or ")} found in #{dist_dir}
|
|
146
|
+
UI.die!("no #{patterns.map { |p| p.delete("*") }.join(" or ")} found in #{dist_dir}. " \
|
|
147
147
|
"run without --skip-build first")
|
|
148
148
|
end
|
|
149
149
|
|
|
@@ -252,7 +252,7 @@ module Everywhere
|
|
|
252
252
|
|
|
253
253
|
def summarize(out, artifact, signing, root_path)
|
|
254
254
|
UI.phase("Release ready 🎉")
|
|
255
|
-
UI.success("#{UI.rel_path(artifact, root_path)} #{UI.dim("(#{(File.size(artifact) / 1024.0 / 1024.0).round}MB
|
|
255
|
+
UI.success("#{UI.rel_path(artifact, root_path)} #{UI.dim("(#{(File.size(artifact) / 1024.0 / 1024.0).round}MB, send this to your friends)")}")
|
|
256
256
|
UI.substep("signed by #{UI.bold(signing["signed_by"] || "?")}")
|
|
257
257
|
UI.substep(signing_line(signing["platform"] || {}, signing))
|
|
258
258
|
UI.substep("receipt #{UI.dim(UI.rel_path(out, root_path))}")
|
|
@@ -26,7 +26,7 @@ module Everywhere
|
|
|
26
26
|
pub_path = File.join(keys_dir, "#{bundle_id}.pub")
|
|
27
27
|
|
|
28
28
|
if File.exist?(sk_path) && !force
|
|
29
|
-
UI.die!("a signing key for #{bundle_id} already exists at #{UI.short_path(sk_path)}
|
|
29
|
+
UI.die!("a signing key for #{bundle_id} already exists at #{UI.short_path(sk_path)}. " \
|
|
30
30
|
"pass --force to overwrite it (shipped apps signed with the old key will REFUSE updates signed with a new one)")
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -44,7 +44,7 @@ module Everywhere
|
|
|
44
44
|
|
|
45
45
|
UI.ok("secret key #{UI.short_path(sk_path)} #{UI.dim("(key id #{pair.key_id_hex})")}")
|
|
46
46
|
if secret.empty?
|
|
47
|
-
UI.warn("this secret key is stored UNENCRYPTED
|
|
47
|
+
UI.warn("this secret key is stored UNENCRYPTED. Anyone who can read " \
|
|
48
48
|
"#{UI.short_path(sk_path)} can sign updates your users will install")
|
|
49
49
|
end
|
|
50
50
|
UI.ok("public key #{UI.short_path(pub_path)}")
|
|
@@ -57,7 +57,7 @@ module Everywhere
|
|
|
57
57
|
public_key: "#{Minisign.public_key_base64(pair.public_key)}"
|
|
58
58
|
|
|
59
59
|
YAML
|
|
60
|
-
UI.substep("back up the secret key somewhere safe
|
|
60
|
+
UI.substep("back up the secret key somewhere safe: losing it strands every shipped app on its current version")
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
private
|
|
@@ -85,7 +85,7 @@ module Everywhere
|
|
|
85
85
|
pass = $stdin.getpass("Passphrase: ")
|
|
86
86
|
confirm = $stdin.getpass("Passphrase (again): ")
|
|
87
87
|
UI.die!("passphrases didn't match") unless pass == confirm
|
|
88
|
-
UI.die!("empty passphrase
|
|
88
|
+
UI.die!("empty passphrase. Rerun without --passphrase for an unencrypted key") if pass.empty?
|
|
89
89
|
pass
|
|
90
90
|
end
|
|
91
91
|
end
|
|
@@ -30,7 +30,7 @@ module Everywhere
|
|
|
30
30
|
value = section["bundle_id"].to_s
|
|
31
31
|
next if value.empty? || (value.match?(BUNDLE_ID) && !value.include?(".."))
|
|
32
32
|
|
|
33
|
-
"#{at}.bundle_id #{value.inspect} is not a bundle id
|
|
33
|
+
"#{at}.bundle_id #{value.inspect} is not a bundle id. Use reverse-DNS: " \
|
|
34
34
|
"letters/digits/./- only (like com.example.app)"
|
|
35
35
|
end + android_target_sdk_errors
|
|
36
36
|
end
|
|
@@ -69,11 +69,11 @@ module Everywhere
|
|
|
69
69
|
tested = AndroidSdk::TARGET_SDK
|
|
70
70
|
|
|
71
71
|
if value.nil?
|
|
72
|
-
["platforms.android.target_sdk #{raw.inspect} is not an API level
|
|
72
|
+
["platforms.android.target_sdk #{raw.inspect} is not an API level: " \
|
|
73
73
|
"a whole number like #{tested}"]
|
|
74
74
|
elsif value < tested
|
|
75
75
|
["platforms.android.target_sdk #{value} is below the shell's tested level " \
|
|
76
|
-
"(#{tested})
|
|
76
|
+
"(#{tested}); this option only raises it, to ship ahead of a Play deadline"]
|
|
77
77
|
else
|
|
78
78
|
[]
|
|
79
79
|
end
|
|
@@ -144,11 +144,11 @@ module Everywhere
|
|
|
144
144
|
def permission_errors(os)
|
|
145
145
|
permissions.flat_map do |name, usage|
|
|
146
146
|
unless MOBILE_PERMISSIONS.include?(name)
|
|
147
|
-
next ["unknown permission #{name.inspect}
|
|
147
|
+
next ["unknown permission #{name.inspect}. Supported: #{MOBILE_PERMISSIONS.join(", ")}"]
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
if os == "ios" && IOS_USAGE_KEYS.key?(name) && usage["ios"].to_s.strip.empty?
|
|
151
|
-
next ["#{name} needs a usage string
|
|
151
|
+
next ["#{name} needs a usage string: the sentence iOS shows when asking. " \
|
|
152
152
|
"In config/everywhere.yml:\n permissions:\n #{name}: \"Why the app needs #{name}.\""]
|
|
153
153
|
end
|
|
154
154
|
|
|
@@ -86,7 +86,7 @@ module Everywhere
|
|
|
86
86
|
errors << "native.desktop.commands must be a list of function names"
|
|
87
87
|
else
|
|
88
88
|
errors += native_desktop_commands.reject { |name| name.match?(RUST_FN_NAME) }.map do |name|
|
|
89
|
-
"native.desktop.commands: #{name.inspect} is not a Rust function name
|
|
89
|
+
"native.desktop.commands: #{name.inspect} is not a Rust function name. " \
|
|
90
90
|
"snake_case (letters, digits, _), matching the fn in native/desktop/"
|
|
91
91
|
end
|
|
92
92
|
duplicates = native_desktop_commands.tally.select { |_name, count| count > 1 }.keys
|
|
@@ -161,7 +161,7 @@ module Everywhere
|
|
|
161
161
|
return [] if version.empty?
|
|
162
162
|
return [] if version.match?(CRATE_VERSION)
|
|
163
163
|
|
|
164
|
-
["#{at} #{version.inspect} is not a cargo version requirement
|
|
164
|
+
["#{at} #{version.inspect} is not a cargo version requirement. Try \"1.2\", \"^1.2.3\" or \"~0.4\""]
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
end
|
|
@@ -110,7 +110,7 @@ module Everywhere
|
|
|
110
110
|
if present.empty?
|
|
111
111
|
errors << "#{at} needs one version requirement (#{PACKAGE_REQUIREMENT_KEYS.join(" / ")})"
|
|
112
112
|
elsif present.length > 1
|
|
113
|
-
errors << "#{at} sets conflicting requirements (#{present.join(", ")})
|
|
113
|
+
errors << "#{at} sets conflicting requirements (#{present.join(", ")}); pick one"
|
|
114
114
|
elsif %w[from exact].include?(present.first) && !entry[present.first].to_s.strip.match?(PACKAGE_VERSION)
|
|
115
115
|
errors << "#{at}.#{present.first} #{entry[present.first].to_s.inspect} must be a version like \"1.2.0\""
|
|
116
116
|
end
|
|
@@ -212,7 +212,7 @@ module Everywhere
|
|
|
212
212
|
coordinate = entry.strip
|
|
213
213
|
next if coordinate.match?(MAVEN_COORDINATE)
|
|
214
214
|
|
|
215
|
-
"#{at} #{coordinate.inspect} is not a Maven coordinate
|
|
215
|
+
"#{at} #{coordinate.inspect} is not a Maven coordinate. " \
|
|
216
216
|
"\"group:artifact:version\" (letters, digits, . _ -)"
|
|
217
217
|
end
|
|
218
218
|
end
|
|
@@ -269,7 +269,7 @@ module Everywhere
|
|
|
269
269
|
def native_android_icon_font_errors
|
|
270
270
|
return [] if ANDROID_ICON_FONTS.key?(native_android_icon_font)
|
|
271
271
|
|
|
272
|
-
["native.android.icon_font #{native_android_icon_font.inspect} is not a known font
|
|
272
|
+
["native.android.icon_font #{native_android_icon_font.inspect} is not a known font. " \
|
|
273
273
|
"one of: #{ANDROID_ICON_FONTS.keys.join(", ")}"]
|
|
274
274
|
end
|
|
275
275
|
|
|
@@ -39,7 +39,7 @@ module Everywhere
|
|
|
39
39
|
return [] if uri.scheme == "https"
|
|
40
40
|
return [] if uri.scheme == "http" && uri.host.to_s.delete("[]").match?(UPDATES_LOOPBACK)
|
|
41
41
|
|
|
42
|
-
["updates.url #{url.inspect} must be https
|
|
42
|
+
["updates.url #{url.inspect} must be https, or the shell disables the updater"]
|
|
43
43
|
rescue URI::Error => e
|
|
44
44
|
["updates.url #{url.inspect} is not a URL (#{e.message})"]
|
|
45
45
|
end
|
|
@@ -50,7 +50,7 @@ module Everywhere
|
|
|
50
50
|
def updates_warnings
|
|
51
51
|
return [] unless updates_url && updates_public_key.to_s.empty?
|
|
52
52
|
|
|
53
|
-
["updates.url is set but updates.public_key is missing
|
|
53
|
+
["updates.url is set but updates.public_key is missing, so the updater will be disabled " \
|
|
54
54
|
"in this build; run `every updates keygen`"]
|
|
55
55
|
end
|
|
56
56
|
|
data/lib/everywhere/database.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Everywhere
|
|
|
31
31
|
elsif defined?(::Sequel)
|
|
32
32
|
prepare_sequel(root)
|
|
33
33
|
else
|
|
34
|
-
log "no ActiveRecord/Sequel detected
|
|
34
|
+
log "no ActiveRecord/Sequel detected, skipping database prepare"
|
|
35
35
|
end
|
|
36
36
|
rescue StandardError => e
|
|
37
37
|
warn "[everywhere] database prepare failed: #{e.class}: #{e.message}"
|
|
@@ -48,7 +48,7 @@ module Everywhere
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
paths = migration_paths(root)
|
|
51
|
-
return log("no migrations found
|
|
51
|
+
return log("no migrations found, skipping") if paths.empty?
|
|
52
52
|
|
|
53
53
|
# Opening the connection creates the sqlite file if it doesn't exist yet.
|
|
54
54
|
::ActiveRecord::Base.connection
|
|
@@ -67,7 +67,7 @@ module Everywhere
|
|
|
67
67
|
return log("no Sequel database handle (set Everywhere.config.sequel_db)") unless db
|
|
68
68
|
|
|
69
69
|
paths = migration_paths(root)
|
|
70
|
-
return log("no migrations found
|
|
70
|
+
return log("no migrations found, skipping") if paths.empty?
|
|
71
71
|
|
|
72
72
|
::Sequel.extension :migration
|
|
73
73
|
paths.each { |dir| ::Sequel::Migrator.run(db, dir) }
|
|
@@ -86,8 +86,8 @@ module Everywhere
|
|
|
86
86
|
files.group_by { |file| file[:name] }.each do |name, group|
|
|
87
87
|
dirs = group.map { |file| file[:dir] }.uniq
|
|
88
88
|
if dirs.length > 1
|
|
89
|
-
errors << "#{name}: same asset name in #{dirs.map { |dir| label(dir) }.join(" and ")}
|
|
90
|
-
"
|
|
89
|
+
errors << "#{name}: same asset name in #{dirs.map { |dir| label(dir) }.join(" and ")}. " \
|
|
90
|
+
"Names are global (the shell reads one flat folder), so rename one"
|
|
91
91
|
end
|
|
92
92
|
group.group_by { |file| [file[:scale], file[:dark]] }.each_value do |clash|
|
|
93
93
|
next if clash.length < 2
|
|
@@ -104,10 +104,10 @@ module Everywhere
|
|
|
104
104
|
def unsupported_error(path, source)
|
|
105
105
|
relative = relative(path, source)
|
|
106
106
|
if VECTOR.include?(File.extname(path).downcase)
|
|
107
|
-
"#{relative}: the desktop shell can't rasterize vectors
|
|
107
|
+
"#{relative}: the desktop shell can't rasterize vectors. Export a PNG " \
|
|
108
108
|
"(at @2x, so it stays sharp on a Retina display)"
|
|
109
109
|
else
|
|
110
|
-
"#{relative}: unsupported image type
|
|
110
|
+
"#{relative}: unsupported image type. Use #{EXTENSIONS.join(", ")}"
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
|
@@ -116,7 +116,7 @@ module Everywhere
|
|
|
116
116
|
name, scale, dark = parse(File.basename(path, File.extname(path)))
|
|
117
117
|
|
|
118
118
|
unless name.match?(VALID_NAME)
|
|
119
|
-
errors << "#{relative}: #{name.inspect} isn't a usable asset name
|
|
119
|
+
errors << "#{relative}: #{name.inspect} isn't a usable asset name. Use letters, digits, _, - and ."
|
|
120
120
|
return nil
|
|
121
121
|
end
|
|
122
122
|
|
|
@@ -106,7 +106,7 @@ module Everywhere
|
|
|
106
106
|
dest
|
|
107
107
|
rescue StandardError => e
|
|
108
108
|
# A Dock icon is never worth failing a dev session over.
|
|
109
|
-
UI.warn("couldn't prepare the Dock icon (#{e.class})
|
|
109
|
+
UI.warn("couldn't prepare the Dock icon (#{e.class}), using the default")
|
|
110
110
|
""
|
|
111
111
|
end
|
|
112
112
|
|
data/lib/everywhere/emulator.rb
CHANGED
|
@@ -113,7 +113,7 @@ module Everywhere
|
|
|
113
113
|
deadline = Time.now + timeout
|
|
114
114
|
bounded_wait(adb_argv(nil, "wait-for-device"), timeout: timeout)
|
|
115
115
|
serial = booted_serial or
|
|
116
|
-
UI.die!("emulator #{name} never registered with adb
|
|
116
|
+
UI.die!("emulator #{name} never registered with adb. Try booting it from Android " \
|
|
117
117
|
"Studio's Device Manager to see what it says")
|
|
118
118
|
wait_until_booted(serial, timeout: [deadline - Time.now, POLL_INTERVAL].max)
|
|
119
119
|
serial
|
|
@@ -187,7 +187,7 @@ module Everywhere
|
|
|
187
187
|
return if status&.success? && !out.include?("Failure [")
|
|
188
188
|
|
|
189
189
|
if application_id && out.include?("INSTALL_FAILED_UPDATE_INCOMPATIBLE")
|
|
190
|
-
UI.warn "#{application_id} is installed with a different signing key
|
|
190
|
+
UI.warn "#{application_id} is installed with a different signing key, reinstalling " \
|
|
191
191
|
"from scratch #{UI.dim("(its app data is dropped)")}"
|
|
192
192
|
uninstall(serial, application_id)
|
|
193
193
|
out, status = capture_install(serial, apk)
|
|
@@ -227,7 +227,7 @@ module Everywhere
|
|
|
227
227
|
*adb_argv(serial, "shell", "am", "start", "-n", retry_component)
|
|
228
228
|
)
|
|
229
229
|
if started?(retry_out, retry_status)
|
|
230
|
-
UI.warn "launched #{resolved}
|
|
230
|
+
UI.warn "launched #{resolved}; #{activity} is stale, update Emulator::DEFAULT_ACTIVITY"
|
|
231
231
|
return
|
|
232
232
|
end
|
|
233
233
|
end
|
|
@@ -320,12 +320,12 @@ module Everywhere
|
|
|
320
320
|
|
|
321
321
|
rewritten = rewrite_loopback(url)
|
|
322
322
|
if emulator?(serial) && rewritten != url
|
|
323
|
-
UI.warn "adb reverse failed
|
|
323
|
+
UI.warn "adb reverse failed, pointing the shell at #{UI.cyan(rewritten)} instead " \
|
|
324
324
|
"#{UI.dim("(#{LOOPBACK_ALIAS} is the emulator's alias for this machine)")}"
|
|
325
325
|
return rewritten
|
|
326
326
|
end
|
|
327
327
|
|
|
328
|
-
UI.warn "adb reverse failed and #{serial || "this target"} isn't an emulator
|
|
328
|
+
UI.warn "adb reverse failed and #{serial || "this target"} isn't an emulator. " \
|
|
329
329
|
"#{UI.cyan(url)} will resolve to the device itself; pass a LAN address " \
|
|
330
330
|
"(--dev-url http://<this machine's IP>:#{port}) with the dev server bound to 0.0.0.0"
|
|
331
331
|
url
|
|
@@ -188,7 +188,7 @@ module Everywhere
|
|
|
188
188
|
when /\Aw: /
|
|
189
189
|
return " #{UI.yellow(short)}" if line.include?("/extensions/")
|
|
190
190
|
|
|
191
|
-
once(:shell_warning) { UI.note_line("Kotlin warnings from the shell template
|
|
191
|
+
once(:shell_warning) { UI.note_line("Kotlin warnings from the shell template; see the full log") }
|
|
192
192
|
when /\ABUILD SUCCESSFUL(?: in (.+))?/
|
|
193
193
|
UI.note_line("build succeeded#{Regexp.last_match(1) && " (#{Regexp.last_match(1)})"}",
|
|
194
194
|
marker: "✓", color: :green)
|
data/lib/everywhere/omniauth.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Everywhere
|
|
|
39
39
|
# re-running replaces the wrapper, never stacks it.
|
|
40
40
|
def protect!(entry_window: ENTRY_WINDOW)
|
|
41
41
|
unless defined?(::OmniAuth) && ::OmniAuth.respond_to?(:config)
|
|
42
|
-
raise "Everywhere::OmniAuth.protect! needs OmniAuth loaded first
|
|
42
|
+
raise "Everywhere::OmniAuth.protect! needs OmniAuth loaded first. " \
|
|
43
43
|
"call it below your OmniAuth/Devise setup (config/initializers/omniauth.rb)"
|
|
44
44
|
end
|
|
45
45
|
|
data/lib/everywhere/paths.rb
CHANGED
|
@@ -91,7 +91,7 @@ module Everywhere
|
|
|
91
91
|
def cache_dir
|
|
92
92
|
File.expand_path(ENV["RUBYEVERYWHERE_HOME"] || "~/.rubyeverywhere")
|
|
93
93
|
rescue ArgumentError
|
|
94
|
-
UI.die!("couldn't resolve your home directory
|
|
94
|
+
UI.die!("couldn't resolve your home directory. Set HOME or RUBYEVERYWHERE_HOME")
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# The stamped copy of the iOS template for one app. Persistent (not a
|
|
@@ -32,7 +32,7 @@ module Everywhere
|
|
|
32
32
|
def self.read_or_warn(path)
|
|
33
33
|
JSON.parse(File.read(path))
|
|
34
34
|
rescue JSON::ParserError, SystemCallError, IOError => e
|
|
35
|
-
Everywhere::UI.warn("ignoring unreadable credentials at #{path} (#{e.class})
|
|
35
|
+
Everywhere::UI.warn("ignoring unreadable credentials at #{path} (#{e.class}). " \
|
|
36
36
|
"delete it and run `every platform login`")
|
|
37
37
|
{}
|
|
38
38
|
end
|
|
@@ -80,7 +80,7 @@ module Everywhere
|
|
|
80
80
|
# message can name it.
|
|
81
81
|
def tab(name)
|
|
82
82
|
unless config && platform
|
|
83
|
-
raise Error, "RequestContext#tab needs the app config
|
|
83
|
+
raise Error, "RequestContext#tab needs the app config. It's available " \
|
|
84
84
|
"inside an Everywhere.tabs block, not to a standalone context"
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -90,7 +90,7 @@ module Everywhere
|
|
|
90
90
|
declared.find { |tab| tab["title"].to_s.casecmp?(wanted) }
|
|
91
91
|
return found if found
|
|
92
92
|
|
|
93
|
-
raise Error, "no tab named #{wanted.inspect} in config/everywhere.yml
|
|
93
|
+
raise Error, "no tab named #{wanted.inspect} in config/everywhere.yml. " \
|
|
94
94
|
"declared: #{declared.map { |tab| tab["title"] }.inspect}"
|
|
95
95
|
end
|
|
96
96
|
end
|
data/lib/everywhere/shellout.rb
CHANGED
|
@@ -41,7 +41,7 @@ module Everywhere
|
|
|
41
41
|
def run!(env, *cmd, chdir: Dir.pwd)
|
|
42
42
|
success = system(child_env(env), *cmd, chdir: chdir)
|
|
43
43
|
# nil means the command never ran (missing binary), false that it failed.
|
|
44
|
-
UI.die!("#{cmd.first} not found
|
|
44
|
+
UI.die!("#{cmd.first} not found. Is it installed and on your PATH?") if success.nil?
|
|
45
45
|
UI.die!("command failed: #{cmd.join(" ")}") unless success
|
|
46
46
|
end
|
|
47
47
|
|
|
@@ -58,7 +58,7 @@ module Everywhere
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def ensure_tool!(name, hint)
|
|
61
|
-
tool?(name) or UI.die!("#{name} not found
|
|
61
|
+
tool?(name) or UI.die!("#{name} not found. #{hint}")
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# quiet also detaches stdin. A quiet command is by definition one that has
|
data/lib/everywhere/simulator.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Everywhere
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
device = devices.reverse.find { |d| d["name"].to_s.match?(/\AiPhone/) } || devices.last
|
|
28
|
-
UI.die!("no iOS Simulator devices
|
|
28
|
+
UI.die!("no iOS Simulator devices. Open Xcode once to install a Simulator runtime") unless device
|
|
29
29
|
|
|
30
30
|
UI.step("booting simulator #{UI.bold(device["name"])}")
|
|
31
31
|
Shellout.run?("xcrun", "simctl", "boot", device["udid"], quiet: true) # races `open` starting it; harmless
|
|
@@ -33,7 +33,7 @@ module Everywhere
|
|
|
33
33
|
# rebuild, no app-store release. Wrap the registration in `to_prepare` so a
|
|
34
34
|
# reload in development doesn't leave the block closed over stale constants.
|
|
35
35
|
def tabs(&block)
|
|
36
|
-
raise ArgumentError, "Everywhere.tabs needs a block
|
|
36
|
+
raise ArgumentError, "Everywhere.tabs needs a block: Everywhere.tabs { |tabs, ctx| … }" unless block
|
|
37
37
|
|
|
38
38
|
@tabs_filter = block
|
|
39
39
|
end
|
|
@@ -68,7 +68,7 @@ module Everywhere
|
|
|
68
68
|
# Return nil for a signed-out request. Runs at most once per request.
|
|
69
69
|
def current_user(&block)
|
|
70
70
|
unless block
|
|
71
|
-
raise ArgumentError, "Everywhere.current_user needs a block
|
|
71
|
+
raise ArgumentError, "Everywhere.current_user needs a block. It registers the resolver " \
|
|
72
72
|
"(ask a RequestContext for the user itself: ctx.current_user)"
|
|
73
73
|
end
|
|
74
74
|
|
data/lib/everywhere/version.rb
CHANGED