ruby_everywhere 0.12.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -33
  3. data/bridge/README.md +23 -23
  4. data/lib/everywhere/android_resources.rb +9 -9
  5. data/lib/everywhere/android_sdk.rb +9 -9
  6. data/lib/everywhere/asset_catalog.rb +7 -7
  7. data/lib/everywhere/blake2b.rb +1 -1
  8. data/lib/everywhere/boot.rb +1 -1
  9. data/lib/everywhere/builders/android.rb +25 -25
  10. data/lib/everywhere/builders/desktop.rb +6 -6
  11. data/lib/everywhere/builders/ios.rb +14 -14
  12. data/lib/everywhere/builders/native_sources.rb +1 -1
  13. data/lib/everywhere/child_supervision.rb +1 -1
  14. data/lib/everywhere/commands/build.rb +54 -24
  15. data/lib/everywhere/commands/clean.rb +1 -1
  16. data/lib/everywhere/commands/dev.rb +11 -11
  17. data/lib/everywhere/commands/doctor.rb +7 -7
  18. data/lib/everywhere/commands/icon.rb +2 -2
  19. data/lib/everywhere/commands/install.rb +21 -21
  20. data/lib/everywhere/commands/logs.rb +6 -6
  21. data/lib/everywhere/commands/platform/auth_status.rb +3 -3
  22. data/lib/everywhere/commands/platform/build.rb +7 -7
  23. data/lib/everywhere/commands/platform/login.rb +5 -5
  24. data/lib/everywhere/commands/platform/logout.rb +1 -1
  25. data/lib/everywhere/commands/platform/runner.rb +6 -6
  26. data/lib/everywhere/commands/preview.rb +14 -14
  27. data/lib/everywhere/commands/publish.rb +14 -14
  28. data/lib/everywhere/commands/release.rb +13 -9
  29. data/lib/everywhere/commands/updates_keygen.rb +4 -4
  30. data/lib/everywhere/config/app.rb +3 -3
  31. data/lib/everywhere/config/mobile.rb +2 -2
  32. data/lib/everywhere/config/native_desktop.rb +2 -2
  33. data/lib/everywhere/config/native_mobile.rb +3 -3
  34. data/lib/everywhere/config/updates.rb +11 -1
  35. data/lib/everywhere/database.rb +3 -3
  36. data/lib/everywhere/desktop_assets.rb +5 -5
  37. data/lib/everywhere/desktop_dev_app.rb +1 -1
  38. data/lib/everywhere/emulator.rb +5 -5
  39. data/lib/everywhere/log_filter.rb +1 -1
  40. data/lib/everywhere/omniauth.rb +1 -1
  41. data/lib/everywhere/paths.rb +1 -1
  42. data/lib/everywhere/platform/credentials.rb +1 -1
  43. data/lib/everywhere/request_context.rb +2 -2
  44. data/lib/everywhere/shellout.rb +2 -2
  45. data/lib/everywhere/simulator.rb +1 -1
  46. data/lib/everywhere/tab_filter.rb +2 -2
  47. data/lib/everywhere/version.rb +1 -1
  48. data/support/agents/AGENTS.md +7 -7
  49. data/support/agents/frameworks/hanami-views.md +8 -8
  50. data/support/agents/frameworks/hanami.md +2 -2
  51. data/support/agents/frameworks/rails-views.md +7 -7
  52. data/support/agents/frameworks/rails.md +8 -8
  53. data/support/agents/frameworks/sinatra-views.md +7 -7
  54. data/support/agents/frameworks/sinatra.md +2 -2
  55. data/support/agents/modes/local.md +2 -2
  56. data/support/agents/modes/remote.md +2 -2
  57. data/support/desktop/src-tauri/src/updater.rs +1 -1
  58. 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 #{config.remote_url} is already online, share that " \
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) nothing to preview")
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 is this machine on a network? (drop --lan to tunnel instead)")
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?} add " \
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} point that rule at the preview, or match it with --port #{svc_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} preview must start the server itself " \
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 Ctrl-C to stop.\n"))
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}) use the link below")
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 scroll up for its output") unless ChildProcesses.alive?(web)
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 starting a fresh one")
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 same URL, same token", marker: "⌁", color: :cyan)
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") ? " your system clock may be off by more than 15 minutes" : ""
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) skipping upload")
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 can't roll back to it") unless s3.head_object(key)
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 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
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 set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY") if
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)} run `every release` first") unless File.exist?(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 — re-run `every release`")
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 set EVERYWHERE_SIGNING_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 #{e.message}")
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 run `every updates keygen` (looked for #{UI.short_path(default)})") unless File.exist?(default)
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
 
@@ -33,7 +33,7 @@ module Everywhere
33
33
 
34
34
  option :root, default: ".", desc: "App root directory"
35
35
  option :target, default: Build::DEFAULT_TARGET, desc: "Build target (os-arch)"
36
- option :channel, default: "direct", desc: "Distribution channel (direct | mac_app_store | ...)"
36
+ option :channel, default: "direct", desc: "Distribution channel (#{Build::CHANNELS.map { |os, chs| "#{os}: #{chs.join("|")}" }.join(" · ")})"
37
37
  option :ruby, default: nil, desc: "Ruby version to package (default: build.ruby or 4.0.6)"
38
38
  option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
39
39
  option :app_name, default: nil, desc: "Bundle name (default from config)"
@@ -50,7 +50,7 @@ module Everywhere
50
50
  ruby ||= config.build_ruby || "4.0.6"
51
51
  os = Everywhere::Config.os_of(target)
52
52
 
53
- preflight!(target, os)
53
+ preflight!(target, os, channel)
54
54
 
55
55
  framework = config.remote? ? nil : Framework.detect(root_path)
56
56
  dist_dir = File.expand_path("dist", root_path)
@@ -82,18 +82,22 @@ module Everywhere
82
82
 
83
83
  private
84
84
 
85
- def preflight!(target, os)
85
+ def preflight!(target, os, channel)
86
86
  case os
87
87
  when "macos"
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 `every release --target #{target}` runs on macOS only") unless Host.darwin?
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} macos-*, ios-* and android-* are supported")
95
+ UI.die!("no release builder for #{target}: macos-*, ios-* and android-* are supported")
96
96
  end
97
+
98
+ # After the os check: the channel table is keyed by os, so an unknown
99
+ # target has to die on the missing builder, not a nil channel list.
100
+ Build.check_channel!(os, channel)
97
101
  end
98
102
 
99
103
 
@@ -112,7 +116,7 @@ module Everywhere
112
116
  # then read the signing facts back off the bundle.
113
117
  def notarize!(dist_dir, root_path, env_file)
114
118
  app_dir = Dir[File.join(dist_dir, "*.app")].max_by { |d| File.mtime(d) }
115
- UI.die!("no .app found in #{dist_dir} run without --skip-build first") unless app_dir
119
+ UI.die!("no .app found in #{dist_dir}. Run without --skip-build first") unless app_dir
116
120
 
117
121
  env_file ||= discover_env_file(root_path)
118
122
  env = env_file ? { "ENV_FILE" => env_file } : {}
@@ -122,7 +126,7 @@ module Everywhere
122
126
  # with, and "which file did that come from" is the first thing you ask
123
127
  # when a build signs as the wrong team.
124
128
  UI.detail("signing env: #{UI.short_path(File.expand_path(env_file))}") if env_file
125
- 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"]
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"]
126
130
  log = "#{app_dir}.notarize.log"
127
131
  Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log, filter: LogFilter.for(:notarize))
128
132
 
@@ -139,7 +143,7 @@ module Everywhere
139
143
  found = patterns.filter_map { |p| Dir[File.join(dist_dir, p)].max_by { |f| File.mtime(f) } }.first
140
144
  return found if found
141
145
 
142
- 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}. " \
143
147
  "run without --skip-build first")
144
148
  end
145
149
 
@@ -248,7 +252,7 @@ module Everywhere
248
252
 
249
253
  def summarize(out, artifact, signing, root_path)
250
254
  UI.phase("Release ready 🎉")
251
- UI.success("#{UI.rel_path(artifact, root_path)} #{UI.dim("(#{(File.size(artifact) / 1024.0 / 1024.0).round}MB send this to your friends)")}")
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)")}")
252
256
  UI.substep("signed by #{UI.bold(signing["signed_by"] || "?")}")
253
257
  UI.substep(signing_line(signing["platform"] || {}, signing))
254
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 anyone who can read " \
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 losing it strands every shipped app on its current version")
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 rerun without --passphrase for an unencrypted key") if pass.empty?
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 reverse-DNS, " \
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}) this option only raises it, to ship ahead of a Play deadline"]
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} — supported: #{MOBILE_PERMISSIONS.join(", ")}"]
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 the sentence iOS shows when asking. " \
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 like \"1.2\", \"^1.2.3\" or \"~0.4\""]
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(", ")}) pick one"
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,11 +39,21 @@ 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 the shell disables the updater otherwise"]
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
46
46
 
47
+ # Not an error — no `updates:` at all is a perfectly good app — but a feed
48
+ # URL without a key drops #updates_shell_hash to nil, so the app ships
49
+ # looking configured with the updater off.
50
+ def updates_warnings
51
+ return [] unless updates_url && updates_public_key.to_s.empty?
52
+
53
+ ["updates.url is set but updates.public_key is missing, so the updater will be disabled " \
54
+ "in this build; run `every updates keygen`"]
55
+ end
56
+
47
57
  # The client subset for the shell; nil (and thus absent from
48
58
  # everywhere.json) until both url and public_key are configured — an
49
59
  # unsigned update feed is not a thing.
@@ -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 skipping database prepare"
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 skipping") if paths.empty?
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 skipping") if paths.empty?
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
- "names are global (the shell reads one flat folder), so rename one"
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 export a PNG " \
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 use #{EXTENSIONS.join(", ")}"
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 use letters, digits, _, - and ."
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}) using the default")
109
+ UI.warn("couldn't prepare the Dock icon (#{e.class}), using the default")
110
110
  ""
111
111
  end
112
112
 
@@ -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 try booting it from Android " \
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 reinstalling " \
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} #{activity} is stale, update Emulator::DEFAULT_ACTIVITY"
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 pointing the shell at #{UI.cyan(rewritten)} instead " \
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 see the full log") }
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)
@@ -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
 
@@ -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 set HOME or RUBYEVERYWHERE_HOME")
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 — it's available " \
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
@@ -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 is it installed and on your PATH?") if success.nil?
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 #{hint}")
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
@@ -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 open Xcode once to install a Simulator runtime") unless device
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