ruby_everywhere 0.6.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 -2
- data/exe/rbe +2 -2
- data/lib/everywhere/agents_guide.rb +3 -1
- data/lib/everywhere/blake2b.rb +17 -1
- data/lib/everywhere/boot.rb +30 -6
- data/lib/everywhere/builders/android.rb +152 -64
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +32 -40
- data/lib/everywhere/builders/ios.rb +268 -36
- 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/cli.rb +2 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +132 -66
- data/lib/everywhere/commands/clean.rb +8 -3
- data/lib/everywhere/commands/dev.rb +91 -242
- data/lib/everywhere/commands/doctor.rb +138 -21
- data/lib/everywhere/commands/install.rb +49 -8
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +141 -33
- data/lib/everywhere/commands/platform/login.rb +20 -6
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +170 -25
- data/lib/everywhere/commands/preview.rb +286 -0
- data/lib/everywhere/commands/publish.rb +22 -2
- data/lib/everywhere/commands/release.rb +155 -35
- data/lib/everywhere/commands/shell_dir.rb +4 -2
- 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 +59 -1367
- data/lib/everywhere/console.rb +2 -1
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/engine.rb +24 -0
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +23 -6
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +13 -5
- data/lib/everywhere/jump.rb +121 -0
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/mobile_config_endpoint.rb +47 -0
- data/lib/everywhere/mobile_configs_controller.rb +46 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +36 -13
- data/lib/everywhere/platform/client.rb +46 -7
- data/lib/everywhere/platform/credentials.rb +18 -8
- data/lib/everywhere/platform/snapshot.rb +12 -0
- 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/receipt.rb +55 -10
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/shellout.rb +19 -0
- data/lib/everywhere/simulator.rb +12 -1
- 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/android/app/build.gradle.kts +29 -1
- data/support/mobile/ios/App/EverywhereConfig.swift +17 -5
- data/support/mobile/ios/App/SceneDelegate.swift +75 -8
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +2 -4
- data/support/mobile/ios/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +0 -1
- data/support/mobile/ios/README.md +13 -6
- data/support/release/macos/notarize.sh +3 -0
- metadata +40 -1
|
@@ -4,6 +4,7 @@ require "dry/cli"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../shellout"
|
|
6
6
|
require_relative "../config"
|
|
7
|
+
require_relative "../framework"
|
|
7
8
|
require_relative "../agents_guide"
|
|
8
9
|
|
|
9
10
|
module Everywhere
|
|
@@ -56,6 +57,7 @@ module Everywhere
|
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def install_sinatra
|
|
60
|
+
add_puma_gem
|
|
59
61
|
vendor_bridge_to_public
|
|
60
62
|
guide_rack_manual_steps(
|
|
61
63
|
db: "point your production SQLite path at ENV.fetch(\"NATIVE_STORAGE_DIR\", \"storage\")",
|
|
@@ -65,6 +67,7 @@ module Everywhere
|
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def install_hanami
|
|
70
|
+
add_puma_gem
|
|
68
71
|
vendor_bridge_to_public
|
|
69
72
|
guide_rack_manual_steps(
|
|
70
73
|
db: "override DATABASE_URL from ENV[\"NATIVE_STORAGE_DIR\"] in config.ru when NATIVE_PACKAGED",
|
|
@@ -100,14 +103,35 @@ module Everywhere
|
|
|
100
103
|
made ? UI.ok(label) : UI.step("#{label} #{UI.dim("(already done, skipped)")}")
|
|
101
104
|
end
|
|
102
105
|
|
|
106
|
+
# A step whose file this app doesn't have. Every one of these is a shape a
|
|
107
|
+
# real app legitimately ships in (API-only, DATABASE_URL, no
|
|
108
|
+
# config/puma.rb), so say what was skipped — never raise on it.
|
|
109
|
+
def skip(label, reason)
|
|
110
|
+
UI.step("#{label} #{UI.dim("(skipped — #{reason})")}")
|
|
111
|
+
end
|
|
112
|
+
|
|
103
113
|
def add_gem
|
|
114
|
+
UI.die!("no Gemfile in #{@root} — run `every install` inside your app") unless File.exist?(app_file("Gemfile"))
|
|
115
|
+
|
|
116
|
+
add_gemfile_gem("ruby_everywhere",
|
|
117
|
+
comment: "# Desktop apps from this #{@framework.name.capitalize} app — https://rubyeverywhere.com")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Packaged Rack apps serve through Puma at runtime (Framework#serve_rack
|
|
121
|
+
# requires it), so a missing gem surfaces as a LoadError on the end user's
|
|
122
|
+
# machine at first launch rather than here.
|
|
123
|
+
def add_puma_gem
|
|
124
|
+
add_gemfile_gem("puma", comment: "# Packaged RubyEverywhere apps serve through puma.")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def add_gemfile_gem(name, comment:)
|
|
104
128
|
gemfile = app_file("Gemfile")
|
|
105
129
|
contents = File.read(gemfile)
|
|
106
|
-
return change("add
|
|
130
|
+
return change("add #{name} to Gemfile", false) if contents.match?(/gem ["']#{name}["']/)
|
|
107
131
|
|
|
108
|
-
File.write(gemfile, "#{contents.chomp}\n\n#
|
|
132
|
+
File.write(gemfile, "#{contents.chomp}\n\n#{comment}\ngem \"#{name}\"\n")
|
|
109
133
|
Shellout.run!({}, "bundle", "install", "--quiet", chdir: @root)
|
|
110
|
-
change("add
|
|
134
|
+
change("add #{name} to Gemfile", true)
|
|
111
135
|
end
|
|
112
136
|
|
|
113
137
|
def write_config
|
|
@@ -214,9 +238,10 @@ module Everywhere
|
|
|
214
238
|
# targets: # the platforms you ship — the CI matrix, expressed once
|
|
215
239
|
# - macos-arm64
|
|
216
240
|
# # - ios-arm64 # Hotwire Native shell; remote mode only. Local builds
|
|
217
|
-
# # # (`every build` / `every dev --ios`)
|
|
218
|
-
# # #
|
|
219
|
-
# # -
|
|
241
|
+
# # # (`every build` / `every dev --ios`) and hosted
|
|
242
|
+
# # # platform builds are both live.
|
|
243
|
+
# # - ios-arm64:testflight # optional per-target distribution channel
|
|
244
|
+
# # - android-arm64 # Hotwire Native shell; remote mode only, hosted builds live
|
|
220
245
|
# capabilities: [notifications, filesystem:read, clipboard] # desktop OS integration
|
|
221
246
|
|
|
222
247
|
# Per-platform overrides. Keys are platform names (macos / ios / android /
|
|
@@ -321,6 +346,8 @@ module Everywhere
|
|
|
321
346
|
|
|
322
347
|
def guard_bootsnap
|
|
323
348
|
boot = app_file("config", "boot.rb")
|
|
349
|
+
return skip("guard bootsnap for packaged runs", "no config/boot.rb") unless File.exist?(boot)
|
|
350
|
+
|
|
324
351
|
contents = File.read(boot)
|
|
325
352
|
return change("guard bootsnap for packaged runs", false) if contents.include?("NATIVE_PACKAGED")
|
|
326
353
|
|
|
@@ -328,12 +355,16 @@ module Everywhere
|
|
|
328
355
|
"# Bootsnap writes its cache to tmp/, which is read-only inside the packaged filesystem.\n" \
|
|
329
356
|
"require \"bootsnap/setup\" unless ENV[\"NATIVE_PACKAGED\"]"
|
|
330
357
|
end
|
|
331
|
-
|
|
332
|
-
|
|
358
|
+
return skip("guard bootsnap for packaged runs", "this app doesn't load bootsnap") unless made
|
|
359
|
+
|
|
360
|
+
File.write(boot, contents)
|
|
361
|
+
change("guard bootsnap for packaged runs", true)
|
|
333
362
|
end
|
|
334
363
|
|
|
335
364
|
def disable_force_ssl
|
|
336
365
|
production = app_file("config", "environments", "production.rb")
|
|
366
|
+
return skip("disable force_ssl for localhost", "no config/environments/production.rb") unless File.exist?(production)
|
|
367
|
+
|
|
337
368
|
contents = File.read(production)
|
|
338
369
|
made = false
|
|
339
370
|
made |= !!contents.gsub!(/^(\s*config\.assume_ssl\s*=\s*)true/) { "#{Regexp.last_match(1)}false # desktop: webview talks plain HTTP to 127.0.0.1" }
|
|
@@ -416,6 +447,11 @@ module Everywhere
|
|
|
416
447
|
# connection was lost" (-1005). Single local user: keep-alives off.
|
|
417
448
|
def disable_puma_keep_alives
|
|
418
449
|
puma = app_file("config", "puma.rb")
|
|
450
|
+
unless File.exist?(puma)
|
|
451
|
+
return skip("disable puma keep-alives when packaged",
|
|
452
|
+
"no config/puma.rb — add `enable_keep_alives false if ENV[\"NATIVE_PACKAGED\"]` to yours if you add one")
|
|
453
|
+
end
|
|
454
|
+
|
|
419
455
|
contents = File.read(puma)
|
|
420
456
|
return change("disable puma keep-alives when packaged", false) if contents.include?("enable_keep_alives")
|
|
421
457
|
|
|
@@ -431,6 +467,11 @@ module Everywhere
|
|
|
431
467
|
|
|
432
468
|
def point_database_at_app_data
|
|
433
469
|
db = app_file("config", "database.yml")
|
|
470
|
+
unless File.exist?(db)
|
|
471
|
+
return skip("point production SQLite at app-data dir",
|
|
472
|
+
"no config/database.yml — point the packaged database at ENV[\"NATIVE_STORAGE_DIR\"] yourself")
|
|
473
|
+
end
|
|
474
|
+
|
|
434
475
|
contents = File.read(db)
|
|
435
476
|
return change("point production SQLite at app-data dir", false) if contents.include?("NATIVE_STORAGE_DIR")
|
|
436
477
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require_relative "../../clock"
|
|
6
7
|
require_relative "../../ui"
|
|
7
8
|
require_relative "../../config"
|
|
8
9
|
require_relative "../../framework"
|
|
@@ -21,8 +22,8 @@ module Everywhere
|
|
|
21
22
|
|
|
22
23
|
option :root, default: ".", desc: "App root directory"
|
|
23
24
|
option :url, default: nil, desc: "Platform base URL"
|
|
24
|
-
option :target, default: nil, desc: "Targets (comma-separated os-arch); overrides everywhere.yml"
|
|
25
|
-
option :channel, default: "direct", desc: "
|
|
25
|
+
option :target, default: nil, desc: "Targets (comma-separated os-arch[:channel]); overrides everywhere.yml"
|
|
26
|
+
option :channel, default: "direct", desc: "Default distribution channel for targets without a :channel suffix"
|
|
26
27
|
option :ruby, default: nil, desc: "Ruby version (default: build.ruby or 4.0.6)"
|
|
27
28
|
option :output, default: nil, desc: "Where to save artifacts (default: <root>/dist)"
|
|
28
29
|
option :wait, type: :boolean, default: true, desc: "Wait for the build and download results"
|
|
@@ -38,29 +39,26 @@ module Everywhere
|
|
|
38
39
|
|
|
39
40
|
client = Everywhere::Platform::Client.new(base, token: creds["token"])
|
|
40
41
|
framework = config.remote? ? nil : Everywhere::Framework.detect(root_path)
|
|
41
|
-
|
|
42
|
+
specs = resolve_targets(target, config, channel)
|
|
42
43
|
ruby ||= config.build_ruby || "4.0.6"
|
|
43
44
|
|
|
44
45
|
Dir.mktmpdir("every-snapshot") do |tmp|
|
|
45
46
|
signed_id = snapshot_and_upload(client, root_path, tmp)
|
|
46
|
-
|
|
47
|
-
root: root_path, config: config, framework: framework, ruby: ruby,
|
|
48
|
-
target: targets.first, channel: channel
|
|
49
|
-
).manifest
|
|
47
|
+
manifests = build_manifests(root_path, config, framework, ruby, specs)
|
|
50
48
|
|
|
51
|
-
build = submit(client,
|
|
52
|
-
UI.success("submitted #{UI.bold(build["id"])} for #{
|
|
49
|
+
build = submit(client, specs, channel, signed_id, manifests)
|
|
50
|
+
UI.success("submitted #{UI.bold(build["id"])} for #{specs.map(&:display_label).join(", ")}")
|
|
53
51
|
if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
|
|
54
52
|
UI.substep("watch it live → #{UI.cyan(build_url)}")
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
return unless wait
|
|
58
56
|
|
|
59
|
-
# Set expectations before the quiet part: a hosted
|
|
57
|
+
# Set expectations before the quiet part: a hosted runner is not
|
|
60
58
|
# instant, and knowing that up front is the difference between "slow"
|
|
61
59
|
# and "broken".
|
|
62
|
-
UI.substep(UI.dim("
|
|
63
|
-
final = poll_and_tail(client, build["id"])
|
|
60
|
+
UI.substep(UI.dim("#{runner_wait_subject(specs)} usually start within a few minutes"))
|
|
61
|
+
final = poll_and_tail(client, build["id"], build_url)
|
|
64
62
|
report(client, final, output || File.join(root_path, "dist"), download)
|
|
65
63
|
end
|
|
66
64
|
rescue Everywhere::Error => e
|
|
@@ -69,6 +67,30 @@ module Everywhere
|
|
|
69
67
|
|
|
70
68
|
private
|
|
71
69
|
|
|
70
|
+
# Which hosted runner OS each target builds on — not the target's own OS
|
|
71
|
+
# (iOS builds on macOS, Android on Linux).
|
|
72
|
+
RUNNER_OS_LABELS = {
|
|
73
|
+
"macos" => "macOS", "ios" => "macOS",
|
|
74
|
+
"android" => "Linux", "linux" => "Linux", "windows" => "Windows"
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
# One requested target. `token` is the literal string the user wrote
|
|
78
|
+
# ("ios-arm64:testflight") and is what keys the per-target manifests on the
|
|
79
|
+
# wire; `os_arch` is the bare form everything else (receipts, labels,
|
|
80
|
+
# filenames) is built from.
|
|
81
|
+
TargetSpec = Struct.new(:token, :os, :arch, :channel) do
|
|
82
|
+
def os_arch = "#{os}-#{arch}"
|
|
83
|
+
def label = UI.target_label(os_arch)
|
|
84
|
+
def display_label = channel == "direct" ? label : "#{label} (#{channel})"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def runner_wait_subject(specs)
|
|
88
|
+
kinds = specs.filter_map { |s| RUNNER_OS_LABELS[s.os] }.uniq
|
|
89
|
+
return "hosted build runners" if kinds.empty?
|
|
90
|
+
|
|
91
|
+
"hosted #{kinds.join("/")} runners"
|
|
92
|
+
end
|
|
93
|
+
|
|
72
94
|
def snapshot_and_upload(client, root_path, tmp)
|
|
73
95
|
path = File.join(tmp, "snapshot.tar.gz")
|
|
74
96
|
UI.step("snapshotting source #{UI.dim("(honoring .everywhereignore)")}")
|
|
@@ -79,6 +101,7 @@ module Everywhere
|
|
|
79
101
|
reservation = client.post("/cli/direct_uploads",
|
|
80
102
|
filename: "snapshot.tar.gz", byte_size: size,
|
|
81
103
|
checksum: Everywhere::Platform::Snapshot.md5_base64(path), content_type: "application/gzip")
|
|
104
|
+
die_unauthorized! if reservation.unauthorized?
|
|
82
105
|
UI.die!("couldn't reserve upload (HTTP #{reservation.status})") unless reservation.ok?
|
|
83
106
|
|
|
84
107
|
upload = reservation.body["direct_upload"] || {}
|
|
@@ -89,21 +112,49 @@ module Everywhere
|
|
|
89
112
|
reservation.body["signed_id"]
|
|
90
113
|
end
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
# `manifests` is keyed by the literal target token; `manifest` (singular)
|
|
116
|
+
# stays = the first target's, which is all an older Platform reads.
|
|
117
|
+
def submit(client, specs, channel, signed_id, manifests)
|
|
93
118
|
res = client.post("/cli/builds",
|
|
94
|
-
targets:
|
|
119
|
+
targets: specs.map(&:token).join(","), channel: channel, snapshot_signed_id: signed_id,
|
|
120
|
+
manifest: manifests.values.first, manifests: manifests)
|
|
95
121
|
return res.body if res.ok?
|
|
96
122
|
|
|
97
|
-
if res.
|
|
123
|
+
die_unauthorized! if res.unauthorized?
|
|
124
|
+
render_submit_error!(res)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def render_submit_error!(res)
|
|
128
|
+
body = res.body.is_a?(Hash) ? res.body : {}
|
|
129
|
+
|
|
130
|
+
case body["error"]
|
|
131
|
+
when "missing_credentials"
|
|
98
132
|
UI.bad("can't build yet — missing signing credentials:")
|
|
99
|
-
Array(
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
Array(body["targets"]).each do |t|
|
|
134
|
+
UI.step("#{server_target_label(t["target"])}: add #{Array(t["missing"]).join(", ")}")
|
|
135
|
+
end
|
|
136
|
+
UI.step("set them up → #{UI.cyan(body["manage_url"])}")
|
|
137
|
+
exit 1
|
|
138
|
+
when "invalid_targets"
|
|
139
|
+
UI.bad(body["message"] || "some targets can't be built as configured:")
|
|
140
|
+
Array(body["targets"]).each do |t|
|
|
141
|
+
UI.step("#{server_target_label(t["target"])}: #{Array(t["errors"]).join("; ")}")
|
|
102
142
|
end
|
|
103
|
-
UI.step("set them up → #{UI.cyan(res.body["manage_url"])}")
|
|
104
143
|
exit 1
|
|
144
|
+
when "unsupported_targets"
|
|
145
|
+
UI.step("supported: #{Array(body["supported"]).join(", ")}") if body["supported"]
|
|
105
146
|
end
|
|
106
|
-
|
|
147
|
+
|
|
148
|
+
UI.die!("build submission failed (HTTP #{res.status}) #{body["message"] || body["error"]}")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def server_target_label(target)
|
|
152
|
+
target ||= {}
|
|
153
|
+
"#{UI.os_label(target["os"])}-#{target["arch"]}/#{target["distribution_channel"]}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def die_unauthorized!
|
|
157
|
+
UI.die!("token expired or revoked — run `every platform login`")
|
|
107
158
|
end
|
|
108
159
|
|
|
109
160
|
# How often we ask the Platform for new logs. A build spends most of its
|
|
@@ -114,14 +165,30 @@ module Everywhere
|
|
|
114
165
|
ACTIVE_POLL = 2
|
|
115
166
|
SPINNER_TICK = 0.2
|
|
116
167
|
|
|
117
|
-
|
|
168
|
+
# The build keeps running on the Platform whether or not we can reach it,
|
|
169
|
+
# so a blip mid-tail must not end the wait. Give up only after this many
|
|
170
|
+
# polls in a row have failed.
|
|
171
|
+
MAX_POLL_ERRORS = 5
|
|
172
|
+
|
|
173
|
+
def poll_and_tail(client, build_id, build_url = nil)
|
|
118
174
|
printed = Hash.new("")
|
|
119
175
|
last = {}
|
|
120
|
-
started =
|
|
176
|
+
started = Clock.monotonic
|
|
121
177
|
status = UI::Status.new
|
|
178
|
+
errors = 0
|
|
122
179
|
|
|
123
180
|
loop do
|
|
124
|
-
|
|
181
|
+
begin
|
|
182
|
+
res = client.get("/cli/builds/#{build_id}/logs")
|
|
183
|
+
errors = 0
|
|
184
|
+
rescue Everywhere::Error => e
|
|
185
|
+
errors += 1
|
|
186
|
+
status.clear
|
|
187
|
+
detach!(build_id, build_url, e) if errors >= MAX_POLL_ERRORS
|
|
188
|
+
UI.warn("lost contact with the Platform (#{e.message}) — retrying #{errors}/#{MAX_POLL_ERRORS}")
|
|
189
|
+
spin(status, [], started, IDLE_POLL)
|
|
190
|
+
next
|
|
191
|
+
end
|
|
125
192
|
artifacts = res.ok? ? (res.body["artifacts"] || []) : []
|
|
126
193
|
|
|
127
194
|
status.clear
|
|
@@ -140,14 +207,24 @@ module Everywhere
|
|
|
140
207
|
status&.clear
|
|
141
208
|
end
|
|
142
209
|
|
|
210
|
+
# We stopped watching; the build didn't stop. Leave the user everything
|
|
211
|
+
# they need to re-attach or grab the artifacts later.
|
|
212
|
+
def detach!(build_id, build_url, error)
|
|
213
|
+
UI.bad("gave up tailing #{UI.bold(build_id)} after #{MAX_POLL_ERRORS} failed polls — #{error.message}")
|
|
214
|
+
UI.step("the build is still running on the Platform")
|
|
215
|
+
UI.step("watch it → #{UI.cyan(build_url)}") if build_url
|
|
216
|
+
UI.die!("re-attach with `every platform build --wait` once you're back online, " \
|
|
217
|
+
"or download the artifacts from the build page")
|
|
218
|
+
end
|
|
219
|
+
|
|
143
220
|
# Hold the status line alive for `interval` seconds. The spinner ticks far
|
|
144
221
|
# faster than we poll so the line reads as "alive" during the multi-minute
|
|
145
222
|
# pre-runner wait rather than freezing between requests.
|
|
146
223
|
def spin(status, artifacts, started, interval)
|
|
147
|
-
deadline =
|
|
224
|
+
deadline = Clock.monotonic + interval
|
|
148
225
|
loop do
|
|
149
|
-
status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(
|
|
150
|
-
remaining = deadline -
|
|
226
|
+
status.update("#{waiting_summary(artifacts)} · #{UI.elapsed(Clock.monotonic - started)} elapsed")
|
|
227
|
+
remaining = deadline - Clock.monotonic
|
|
151
228
|
break if remaining <= 0
|
|
152
229
|
|
|
153
230
|
sleep([remaining, SPINNER_TICK].min)
|
|
@@ -172,8 +249,6 @@ module Everywhere
|
|
|
172
249
|
label.empty? ? artifact["status"].to_s : label
|
|
173
250
|
end
|
|
174
251
|
|
|
175
|
-
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
176
|
-
|
|
177
252
|
def tail_log(artifact, printed)
|
|
178
253
|
full = artifact["log"].to_s
|
|
179
254
|
prev = printed[artifact["id"]]
|
|
@@ -224,11 +299,44 @@ module Everywhere
|
|
|
224
299
|
end
|
|
225
300
|
end
|
|
226
301
|
|
|
227
|
-
# everywhere.yml build.targets, or --target, or the macOS default.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
302
|
+
# everywhere.yml build.targets, or --target, or the macOS default. Each
|
|
303
|
+
# entry is "os-arch" with an optional ":channel" suffix; --channel is the
|
|
304
|
+
# default for the ones that leave it off. No channel allowlist here — the
|
|
305
|
+
# Platform owns which channels exist and says so in its 422.
|
|
306
|
+
def resolve_targets(override, config, default_channel)
|
|
307
|
+
list = override ? override.split(",") : config.build_target_specs
|
|
308
|
+
list = Array(list).map { |t| t.to_s.strip }.reject(&:empty?)
|
|
309
|
+
list = ["macos-arm64"] if list.empty?
|
|
310
|
+
list.map { |token| parse_target_spec(token, default_channel) }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def parse_target_spec(token, default_channel)
|
|
314
|
+
os_arch, channel, extra = token.split(":", -1)
|
|
315
|
+
if !extra.nil? || os_arch.to_s.empty? || (!channel.nil? && channel.empty?)
|
|
316
|
+
UI.die!("invalid target #{token.inspect} — expected os-arch[:channel], e.g. ios-arm64:testflight")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
os, arch = os_arch.split("-", 2)
|
|
320
|
+
TargetSpec.new(token, os, arch, channel || default_channel)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# One manifest per target — they differ in `target`, and may differ in
|
|
324
|
+
# bundle_id/name/version via `platforms:` overrides. The source digest is
|
|
325
|
+
# the same for all of them, so hash the tree once and thread it through.
|
|
326
|
+
def build_manifests(root_path, config, framework, ruby, specs)
|
|
327
|
+
source = nil
|
|
328
|
+
lockfile = nil
|
|
329
|
+
|
|
330
|
+
specs.each_with_object({}) do |spec, out|
|
|
331
|
+
manifest = Everywhere::Receipt.new(
|
|
332
|
+
root: root_path, config: config, framework: framework, ruby: ruby,
|
|
333
|
+
target: spec.os_arch, channel: spec.channel,
|
|
334
|
+
source_checksum: source, lockfile_checksum: lockfile
|
|
335
|
+
).manifest
|
|
336
|
+
source ||= manifest.dig("source", "checksum")
|
|
337
|
+
lockfile ||= manifest.dig("source", "lockfile_checksum")
|
|
338
|
+
out[spec.token] = manifest
|
|
339
|
+
end
|
|
232
340
|
end
|
|
233
341
|
|
|
234
342
|
def label(target)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require "socket"
|
|
5
|
+
require_relative "../../clock"
|
|
6
|
+
require_relative "../../error"
|
|
5
7
|
require_relative "../../ui"
|
|
6
8
|
require_relative "../../platform/client"
|
|
7
9
|
require_relative "../../platform/credentials"
|
|
@@ -23,6 +25,14 @@ module Everywhere
|
|
|
23
25
|
start = client.post("/cli/device_authorizations", client_name: client_name)
|
|
24
26
|
UI.die!("couldn't start login — HTTP #{start.status}") unless start.ok?
|
|
25
27
|
|
|
28
|
+
# A captive portal or a misconfigured proxy answers 200 with HTML, which
|
|
29
|
+
# parses to {} — without this we'd print an empty code and poll nothing
|
|
30
|
+
# for ten minutes.
|
|
31
|
+
unless start.body["user_code"] && start.body["device_code"]
|
|
32
|
+
UI.die!("#{base} answered without a device code — check EVERYWHERE_PLATFORM_URL " \
|
|
33
|
+
"(a captive portal or proxy may be intercepting the request)")
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
verify_url = start.body["verification_uri_complete"] || start.body["verification_uri"]
|
|
27
37
|
UI.step("confirmation code: #{UI.bold(start.body["user_code"])}")
|
|
28
38
|
UI.step("opening #{UI.cyan(verify_url)}")
|
|
@@ -43,14 +53,20 @@ module Everywhere
|
|
|
43
53
|
private
|
|
44
54
|
|
|
45
55
|
def poll(client, device_code, interval, expires_in)
|
|
46
|
-
deadline = monotonic + (expires_in.positive? ? expires_in : 600)
|
|
56
|
+
deadline = Clock.monotonic + (expires_in.positive? ? expires_in : 600)
|
|
47
57
|
loop do
|
|
48
58
|
sleep(interval)
|
|
49
|
-
UI.die!("login timed out — run `every platform login` again") if monotonic > deadline
|
|
59
|
+
UI.die!("login timed out — run `every platform login` again") if Clock.monotonic > deadline
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
# The user is mid-approval in a browser; a dropped connection here must
|
|
62
|
+
# not kill the login. Keep polling until the deadline instead.
|
|
63
|
+
res = begin
|
|
64
|
+
client.post("/cli/device_authorizations/token", device_code: device_code)
|
|
65
|
+
rescue Everywhere::Error
|
|
66
|
+
next
|
|
67
|
+
end
|
|
52
68
|
UI.die!("login failed — #{res.body["error"] || "invalid request"}") if res.status == 400
|
|
53
|
-
next unless res.ok? # transient 5xx
|
|
69
|
+
next unless res.ok? # transient 5xx: keep polling until the deadline
|
|
54
70
|
|
|
55
71
|
case res.body["status"]
|
|
56
72
|
when "approved" then return res.body
|
|
@@ -61,8 +77,6 @@ module Everywhere
|
|
|
61
77
|
end
|
|
62
78
|
end
|
|
63
79
|
|
|
64
|
-
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
65
|
-
|
|
66
80
|
# Shown on the approval page + used as the token's name, so a user can tell
|
|
67
81
|
# their machines apart when reviewing/revoking tokens.
|
|
68
82
|
def client_name
|