ruby_everywhere 0.1.4 → 0.1.5
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/lib/everywhere/commands/build.rb +4 -3
- data/lib/everywhere/commands/platform/build.rb +3 -0
- data/lib/everywhere/commands/platform/runner.rb +22 -9
- data/lib/everywhere/commands/release.rb +13 -13
- data/lib/everywhere/log_filter.rb +148 -0
- data/lib/everywhere/shellout.rb +36 -5
- data/lib/everywhere/ui.rb +70 -11
- data/lib/everywhere/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8f07eb6ae976f35ea7ccda33b87bfefd7c2d0ffd39c1b7aa1f7001bedbd482b
|
|
4
|
+
data.tar.gz: b0b9afc49cdc99e731c5bc7a742f8a90c4a5c94d496749131fa37b403567f91c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f13c6059ba2dae4dbe87c21a376265ae7cef7f84a890492897694dde101e9b94bc4990df82b6efc64f278e1ed35d3d921e8bfd7d5c131763cc79589d096d4211
|
|
7
|
+
data.tar.gz: 741d54c87c703df48024662485d03edcc15546c20f3fbe6f6d52b2b3c6056658c8aff7f2e73eb558d7f31961aa1c65c686a656bfc5e9bf9c2d900a3c633057aa
|
|
@@ -5,6 +5,7 @@ require "fileutils"
|
|
|
5
5
|
require "tmpdir"
|
|
6
6
|
require "shellwords"
|
|
7
7
|
require_relative "../shellout"
|
|
8
|
+
require_relative "../log_filter"
|
|
8
9
|
require_relative "../png"
|
|
9
10
|
require_relative "../icon"
|
|
10
11
|
require_relative "../paths"
|
|
@@ -89,11 +90,11 @@ module Everywhere
|
|
|
89
90
|
staging = File.join(Dir.mktmpdir("everywhere-press"), File.basename(output_path))
|
|
90
91
|
log = "#{output_path}.build.log"
|
|
91
92
|
|
|
92
|
-
UI.step("rbe-tebako press (Ruby #{ruby}
|
|
93
|
-
UI.
|
|
93
|
+
UI.step("rbe-tebako press #{UI.dim("(Ruby #{ruby} — grab a coffee on first run)")}")
|
|
94
|
+
UI.detail("full build log: #{UI.short_path(log)}")
|
|
94
95
|
Shellout.run_logged!(press_env, ["rbe-tebako", "press",
|
|
95
96
|
"--root=#{framework.root}", "--entry-point=#{entry}",
|
|
96
|
-
"--Ruby=#{ruby}", "--output=#{staging}"], log: log)
|
|
97
|
+
"--Ruby=#{ruby}", "--output=#{staging}"], log: log, filter: LogFilter.for(:tebako))
|
|
97
98
|
|
|
98
99
|
# Tebako's final strip step only warns (and still exits 0) if it can't
|
|
99
100
|
# write the output, so verify the artifact instead of trusting exit codes.
|
|
@@ -50,6 +50,9 @@ module Everywhere
|
|
|
50
50
|
|
|
51
51
|
build = submit(client, targets, channel, signed_id, manifest)
|
|
52
52
|
UI.success("submitted #{UI.bold(build["id"])} for #{targets.join(", ")}")
|
|
53
|
+
if (build_url = build["url"] || "#{client.base_url}/builds/#{build["id"]}")
|
|
54
|
+
UI.substep("watch it live → #{UI.cyan(build_url)}")
|
|
55
|
+
end
|
|
53
56
|
|
|
54
57
|
return unless wait
|
|
55
58
|
|
|
@@ -31,13 +31,18 @@ module Everywhere
|
|
|
31
31
|
option :shell_dir, default: nil, desc: "Path to the generic Tauri shell (src-tauri)"
|
|
32
32
|
|
|
33
33
|
def call(artifact:, token:, url: nil, shell_dir: nil, **)
|
|
34
|
+
# We run headless on a build runner (a pipe, not a TTY) and stream into
|
|
35
|
+
# the Platform web log, which renders ANSI. Force color on so the hosted
|
|
36
|
+
# log reads like a local run.
|
|
37
|
+
ENV["CLICOLOR_FORCE"] ||= "1" unless ENV["NO_COLOR"]
|
|
38
|
+
|
|
34
39
|
base = Everywhere::Platform::Client.base_url(url)
|
|
35
40
|
@client = Everywhere::Platform::Client.new(base, token: token)
|
|
36
41
|
@artifact = artifact
|
|
37
42
|
|
|
38
43
|
job = get!("/runner/artifacts/#{artifact}")
|
|
39
44
|
patch!("/runner/artifacts/#{artifact}", status: "running")
|
|
40
|
-
remote_log("
|
|
45
|
+
remote_log("picked up #{UI.cyan("#{job.dig("target", "os")}-#{job.dig("target", "arch")}")} for #{UI.bold(job.dig("app", "name"))}")
|
|
41
46
|
|
|
42
47
|
Dir.mktmpdir("every-runner") do |work|
|
|
43
48
|
src = fetch_snapshot(work, job)
|
|
@@ -63,7 +68,7 @@ module Everywhere
|
|
|
63
68
|
# ---- pipeline steps ------------------------------------------------------
|
|
64
69
|
|
|
65
70
|
def fetch_snapshot(work, job)
|
|
66
|
-
remote_log("
|
|
71
|
+
remote_log("downloading source snapshot")
|
|
67
72
|
tarball = File.join(work, "snapshot.tar.gz")
|
|
68
73
|
@client.download(job["snapshot_url"], tarball)
|
|
69
74
|
src = File.join(work, "src")
|
|
@@ -73,7 +78,7 @@ module Everywhere
|
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
def pull_credentials(work)
|
|
76
|
-
remote_log("
|
|
81
|
+
remote_log("pulling signing credentials")
|
|
77
82
|
creds = get!("/runner/artifacts/#{@artifact}/credentials")["credentials"] || []
|
|
78
83
|
env = {}
|
|
79
84
|
creds.each do |cred|
|
|
@@ -113,19 +118,23 @@ module Everywhere
|
|
|
113
118
|
listing = `security find-identity -p codesigning #{@keychain.shellescape} 2>/dev/null`
|
|
114
119
|
sha = listing[/\b([0-9A-F]{40})\b/, 1]
|
|
115
120
|
UI.die!("no codesigning identity in the delivered .p12") unless sha
|
|
116
|
-
remote_log("
|
|
121
|
+
remote_log("signing identity #{listing[/"(.+)"/, 1]} #{UI.dim("(#{sha[0, 10]}…)")}")
|
|
117
122
|
sha
|
|
118
123
|
end
|
|
119
124
|
|
|
120
125
|
def run_release(src, job, signing_env, shell_dir)
|
|
121
|
-
remote_log("
|
|
126
|
+
remote_log("every release — press → sign → notarize → staple")
|
|
122
127
|
cmd = [RbConfig.ruby, "-I", lib_dir, every_exe, "release",
|
|
123
128
|
"--root", src,
|
|
124
129
|
"--target", "#{job.dig("target", "os")}-#{job.dig("target", "arch")}",
|
|
125
130
|
"--channel", job.dig("target", "distribution_channel").to_s]
|
|
126
131
|
cmd += ["--shell-dir", shell_dir] if shell_dir
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
# The web UI renders ANSI, but `every release` runs here as a pipe (not a
|
|
134
|
+
# TTY) so it would otherwise strip all color. Force it on — for our own
|
|
135
|
+
# output and for cargo — so the hosted log looks like the local one.
|
|
136
|
+
forced = { "CLICOLOR_FORCE" => "1", "CARGO_TERM_COLOR" => "always" }
|
|
137
|
+
success = stream(forced.merge(signing_env), cmd)
|
|
129
138
|
|
|
130
139
|
dist = File.join(src, "dist")
|
|
131
140
|
release_json = JSON.parse(File.read(File.join(dist, "release.json"))) rescue nil
|
|
@@ -134,7 +143,7 @@ module Everywhere
|
|
|
134
143
|
end
|
|
135
144
|
|
|
136
145
|
def complete_success(zip, release_json)
|
|
137
|
-
remote_log("
|
|
146
|
+
remote_log("uploading artifact #{UI.dim("(#{(File.size(zip) / 1024.0 / 1024.0).round(1)}MB)")}")
|
|
138
147
|
signed_id = upload_artifact(zip)
|
|
139
148
|
post!("/runner/artifacts/#{@artifact}/complete",
|
|
140
149
|
status: "succeeded", file_signed_id: signed_id, release_json: release_json)
|
|
@@ -185,9 +194,13 @@ module Everywhere
|
|
|
185
194
|
nil # never let a log hiccup kill the build
|
|
186
195
|
end
|
|
187
196
|
|
|
197
|
+
# A runner-orchestration line: distinct from the `every release` steps it
|
|
198
|
+
# brackets (tagged "runner"), rendered once and forwarded verbatim — color
|
|
199
|
+
# and all — to the web log.
|
|
188
200
|
def remote_log(message)
|
|
189
|
-
UI.
|
|
190
|
-
|
|
201
|
+
line = "#{UI.gray("runner")} #{UI.cyan("→")} #{message}"
|
|
202
|
+
puts line
|
|
203
|
+
flush_log("#{line}\n")
|
|
191
204
|
end
|
|
192
205
|
|
|
193
206
|
# ---- helpers -------------------------------------------------------------
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
4
|
require_relative "../shellout"
|
|
5
|
+
require_relative "../log_filter"
|
|
5
6
|
require_relative "../receipt"
|
|
6
7
|
require_relative "../paths"
|
|
7
8
|
require_relative "build"
|
|
@@ -48,7 +49,7 @@ module Everywhere
|
|
|
48
49
|
# 1. Produce (or reuse) the signed-for-dev .app via the build path. The
|
|
49
50
|
# notarize step below re-signs it inside-out with the Developer ID.
|
|
50
51
|
unless skip_build
|
|
51
|
-
UI.
|
|
52
|
+
UI.phase("Building #{app_name} for #{target}")
|
|
52
53
|
Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, target: target, shell_dir: shell_dir, **)
|
|
53
54
|
end
|
|
54
55
|
|
|
@@ -58,10 +59,11 @@ module Everywhere
|
|
|
58
59
|
# 2. Sign (Developer ID + hardened runtime) → notarize → staple → verify.
|
|
59
60
|
env_file ||= discover_env_file(root_path)
|
|
60
61
|
env = env_file ? { "ENV_FILE" => env_file } : {}
|
|
61
|
-
UI.
|
|
62
|
+
UI.phase("Signing & notarizing #{File.basename(app_dir)}")
|
|
63
|
+
UI.step("preparing #{File.basename(app_dir)}#{env_file ? " #{UI.dim("(env: #{rel(env_file, root_path)})")}" : ""}")
|
|
62
64
|
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"]
|
|
63
65
|
log = "#{app_dir}.notarize.log"
|
|
64
|
-
Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log)
|
|
66
|
+
Shellout.run_logged!(env, ["/bin/bash", NOTARIZE, app_dir], log: log, filter: LogFilter.for(:notarize))
|
|
65
67
|
|
|
66
68
|
# 3. Gather signing facts from the bundle itself, then emit the receipt.
|
|
67
69
|
artifact = "#{app_dir.sub(/\.app\z/, "")}.zip" # notarize.sh re-zips the stapled app
|
|
@@ -125,18 +127,16 @@ module Everywhere
|
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
def summarize(out, artifact, signing, root_path)
|
|
128
|
-
UI.
|
|
129
|
-
UI.
|
|
130
|
-
UI.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
UI.
|
|
130
|
+
UI.phase("Release ready 🎉")
|
|
131
|
+
UI.success("#{rel(artifact, root_path)} #{UI.dim("(#{(File.size(artifact) / 1024.0 / 1024.0).round}MB — send this to your friends)")}")
|
|
132
|
+
UI.substep("signed by #{UI.bold(signing["signed_by"] || "?")}")
|
|
133
|
+
UI.substep("notarized #{UI.yn(signing.dig("platform", "notarized"))} · " \
|
|
134
|
+
"stapled #{UI.yn(signing.dig("platform", "stapled"))} · " \
|
|
135
|
+
"gatekeeper #{UI.yn(signing["verified"])}")
|
|
136
|
+
UI.substep("receipt #{UI.dim(rel(out, root_path))}")
|
|
135
137
|
end
|
|
136
138
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
def rel(path, root_path) = path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", "")
|
|
139
|
+
def rel(path, root_path) = UI.short_path(path.sub("#{root_path}/", "").sub("#{Dir.pwd}/", ""))
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ui"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# Turns the raw, chatty output of the external build tools (tebako, the
|
|
7
|
+
# notarize script and the Apple tools it drives) into clean, indented, colored
|
|
8
|
+
# lines for the console and the Platform web log.
|
|
9
|
+
#
|
|
10
|
+
# A filter is a small state machine fed one raw line at a time. It returns:
|
|
11
|
+
# * a String -> print this (already colored/indented) instead of the raw line
|
|
12
|
+
# * :drop -> swallow the line (pure noise: repeated status, temp paths…)
|
|
13
|
+
# * nil -> not recognized; caller shows the raw line dimmed as a detail
|
|
14
|
+
#
|
|
15
|
+
# The FULL raw stream is always still written to the on-disk log, so nothing is
|
|
16
|
+
# lost — this only governs what a human sees scroll by.
|
|
17
|
+
class LogFilter
|
|
18
|
+
def self.for(profile) = new(profile)
|
|
19
|
+
|
|
20
|
+
def initialize(profile)
|
|
21
|
+
@profile = profile
|
|
22
|
+
@seen = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(raw)
|
|
26
|
+
line = raw.to_s.rstrip
|
|
27
|
+
return :drop if line.empty?
|
|
28
|
+
|
|
29
|
+
case @profile
|
|
30
|
+
when :notarize then notarize(line)
|
|
31
|
+
when :tebako then tebako(line)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# --- `every release` signing pipeline (notarize.sh + Apple tools) ---------
|
|
38
|
+
|
|
39
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
40
|
+
def notarize(line)
|
|
41
|
+
case line
|
|
42
|
+
# notarize.sh's own "==> phase" markers become substeps.
|
|
43
|
+
when /\A==> signing with:\s*(.+)/
|
|
44
|
+
UI.substep_line("signing bundle #{UI.dim("(#{short_identity(Regexp.last_match(1))})")}")
|
|
45
|
+
when /\A==> verifying signature/
|
|
46
|
+
UI.substep_line("verifying signature")
|
|
47
|
+
when /\A==> zipping for notarization:\s*(.+)/
|
|
48
|
+
UI.substep_line("zipping for notarization #{UI.dim("→ #{File.basename(Regexp.last_match(1))}")}")
|
|
49
|
+
when /\A==> submitting to Apple notary service/
|
|
50
|
+
UI.substep_line("submitting to Apple notary service #{UI.dim("(this can take a few minutes)")}")
|
|
51
|
+
when /\A==> stapling ticket/
|
|
52
|
+
UI.substep_line("stapling notarization ticket")
|
|
53
|
+
when /\A==> Gatekeeper assessment/
|
|
54
|
+
UI.substep_line("Gatekeeper assessment")
|
|
55
|
+
when /\A==> re-zipping/
|
|
56
|
+
UI.substep_line("re-zipping stapled app for distribution")
|
|
57
|
+
|
|
58
|
+
# Per-binary signing chatter -> one dim line each (basename only).
|
|
59
|
+
when /\A\s+sign (.+)/
|
|
60
|
+
UI.note_line("signed #{File.basename(Regexp.last_match(1))}")
|
|
61
|
+
when /: replacing existing signature/
|
|
62
|
+
:drop
|
|
63
|
+
|
|
64
|
+
# codesign --verify / stapler / spctl outcomes.
|
|
65
|
+
when /: valid on disk/
|
|
66
|
+
UI.note_line("valid on disk", marker: "✓", color: :green)
|
|
67
|
+
when /: satisfies its Designated Requirement/
|
|
68
|
+
:drop
|
|
69
|
+
when /The staple and validate action worked!/
|
|
70
|
+
UI.note_line("stapled and validated", marker: "✓", color: :green)
|
|
71
|
+
when /The validate action worked!/
|
|
72
|
+
:drop
|
|
73
|
+
when /\A(.+): accepted/
|
|
74
|
+
UI.note_line("Gatekeeper accepted", marker: "✓", color: :green)
|
|
75
|
+
when /\Asource=/, /\Aorigin=/
|
|
76
|
+
UI.note_line(line)
|
|
77
|
+
|
|
78
|
+
# notarytool --wait progress. Collapse the repeated dotted "In Progress"
|
|
79
|
+
# lines into a single heartbeat, and celebrate the terminal states.
|
|
80
|
+
when /Current status:\s*Accepted/
|
|
81
|
+
UI.note_line("notarization accepted", marker: "✓", color: :green)
|
|
82
|
+
when /Current status:\s*(Invalid|Rejected)/
|
|
83
|
+
UI.note_line("notarization #{Regexp.last_match(1).downcase}", marker: "✗", color: :red)
|
|
84
|
+
when /Current status:\s*In Progress/
|
|
85
|
+
once(:progress) { UI.note_line("waiting on Apple…", marker: "⋯") }
|
|
86
|
+
when /\A\s*id:\s*([0-9a-fA-F-]{36})/
|
|
87
|
+
once(:submission) { UI.note_line("submission #{Regexp.last_match(1)[0, 8]}…") }
|
|
88
|
+
|
|
89
|
+
# Pure noise from notarytool's own prose / the final banner (release.rb
|
|
90
|
+
# prints its own summary, so drop notarize.sh's).
|
|
91
|
+
when /\AConducting pre-submission checks/,
|
|
92
|
+
/\ASubmission ID received/,
|
|
93
|
+
/\ASuccessfully uploaded file/,
|
|
94
|
+
/\A\s*path:/,
|
|
95
|
+
/\A\s*status:/,
|
|
96
|
+
/\AWaiting for processing/,
|
|
97
|
+
/\AProcessing:/,
|
|
98
|
+
/Processing complete/,
|
|
99
|
+
/\A✅ Done\./,
|
|
100
|
+
/Send this to your friend/,
|
|
101
|
+
/Stapled, so it works even offline/,
|
|
102
|
+
/zipping preserves the signature/
|
|
103
|
+
:drop
|
|
104
|
+
|
|
105
|
+
else
|
|
106
|
+
nil # unrecognized -> caller shows it dimmed
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
110
|
+
|
|
111
|
+
# --- tebako press ---------------------------------------------------------
|
|
112
|
+
#
|
|
113
|
+
# Thousands of lines of C++/Ruby build output. We can't restructure it, but
|
|
114
|
+
# we can keep errors/warnings legible and dim the rest into the background so
|
|
115
|
+
# the important lines (and our own steps around it) stand out.
|
|
116
|
+
def tebako(line)
|
|
117
|
+
short = UI.short_path(line)
|
|
118
|
+
case line
|
|
119
|
+
when /\b(error|fatal|undefined reference|cannot find)\b/i
|
|
120
|
+
" #{UI.red(short)}"
|
|
121
|
+
when /\b(warning|deprecated)\b/i
|
|
122
|
+
" #{UI.yellow(short)}"
|
|
123
|
+
when /\A-- /, /\bBuilding\b/, /\bConfiguring\b/, /\bInstalling\b/
|
|
124
|
+
UI.note_line(short.sub(/\A-- /, ""))
|
|
125
|
+
else
|
|
126
|
+
" #{UI.gray(short)}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Show a line only the first time its key is seen; :drop the repeats. Keeps a
|
|
131
|
+
# long notarization from scrolling a wall of identical "waiting" lines (and
|
|
132
|
+
# notarytool's thrice-echoed submission id from showing three times).
|
|
133
|
+
def once(key)
|
|
134
|
+
return :drop if @seen[key]
|
|
135
|
+
|
|
136
|
+
@seen[key] = true
|
|
137
|
+
yield
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# "Developer ID Application: Andrea Fomera (TU4485Z3KR)" or a bare SHA-1.
|
|
141
|
+
def short_identity(id)
|
|
142
|
+
id = id.strip
|
|
143
|
+
return "#{id[0, 10]}…" if id.match?(/\A[0-9A-F]{40}\z/i)
|
|
144
|
+
|
|
145
|
+
id[/\(([^)]+)\)\s*\z/, 1] || id
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/everywhere/shellout.rb
CHANGED
|
@@ -31,14 +31,45 @@ module Everywhere
|
|
|
31
31
|
["", nil]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Run a command
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
# Run a command, capturing its full combined output to a log file while
|
|
35
|
+
# showing a (optionally filtered) view on the console.
|
|
36
|
+
#
|
|
37
|
+
# Pass filter: a LogFilter (or any #call(line) -> String | :drop | nil) to
|
|
38
|
+
# prettify what a human sees; the on-disk log always gets the raw stream, so
|
|
39
|
+
# nothing is lost. Unrecognized lines (filter returns nil) are shown dimmed
|
|
40
|
+
# and indented so they read as background detail under the current step.
|
|
41
|
+
def run_logged!(env, cmd, log:, filter: nil)
|
|
42
|
+
require "open3"
|
|
43
|
+
success = false
|
|
44
|
+
unbundled do
|
|
45
|
+
File.open(log, "w") do |logf|
|
|
46
|
+
Open3.popen2e(env, *cmd) do |stdin, out, wait|
|
|
47
|
+
stdin.close
|
|
48
|
+
out.each_line do |raw|
|
|
49
|
+
logf.write(raw)
|
|
50
|
+
emit(raw, filter)
|
|
51
|
+
end
|
|
52
|
+
success = wait.value.success?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
39
56
|
UI.die!("command failed: #{cmd.join(" ")} (full log: #{log})") unless success
|
|
40
57
|
end
|
|
41
58
|
|
|
59
|
+
# Route one raw line to the console: verbatim when there's no filter, else
|
|
60
|
+
# the filter's rendition (:drop swallows it, nil falls back to dim detail).
|
|
61
|
+
def emit(raw, filter)
|
|
62
|
+
return $stdout.print(raw) unless filter
|
|
63
|
+
|
|
64
|
+
shown = filter.call(raw)
|
|
65
|
+
case shown
|
|
66
|
+
when :drop, false then nil
|
|
67
|
+
when nil then $stdout.puts(" #{UI.gray(UI.short_path(raw.rstrip))}")
|
|
68
|
+
else $stdout.puts(shown)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
private_class_method :emit
|
|
72
|
+
|
|
42
73
|
def spawn(env, cmd, chdir:)
|
|
43
74
|
unbundled { Process.spawn(env, cmd, chdir: chdir) }
|
|
44
75
|
end
|
data/lib/everywhere/ui.rb
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
module Everywhere
|
|
4
4
|
# Terminal output helpers. Colors follow the informal standard:
|
|
5
5
|
# on when stdout is a TTY or CLICOLOR_FORCE=1, always off when NO_COLOR is set.
|
|
6
|
+
#
|
|
7
|
+
# The hosted runner streams `every release` through a pipe (not a TTY) into the
|
|
8
|
+
# Platform web log, so it exports CLICOLOR_FORCE=1 to keep our color — the web
|
|
9
|
+
# UI renders ANSI. That's why we force rather than sniff the TTY there.
|
|
6
10
|
module UI
|
|
7
11
|
module_function
|
|
8
12
|
|
|
@@ -17,21 +21,76 @@ module Everywhere
|
|
|
17
21
|
color? ? "\e[#{codes.join(";")}m#{text}\e[0m" : text.to_s
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def bold(t)
|
|
21
|
-
def dim(t)
|
|
22
|
-
def red(t)
|
|
23
|
-
def green(t)
|
|
24
|
-
def yellow(t)
|
|
25
|
-
def
|
|
24
|
+
def bold(t) = paint(t, 1)
|
|
25
|
+
def dim(t) = paint(t, 2)
|
|
26
|
+
def red(t) = paint(t, 31)
|
|
27
|
+
def green(t) = paint(t, 32)
|
|
28
|
+
def yellow(t) = paint(t, 33)
|
|
29
|
+
def blue(t) = paint(t, 34)
|
|
30
|
+
def magenta(t) = paint(t, 35)
|
|
31
|
+
def cyan(t) = paint(t, 36)
|
|
32
|
+
def gray(t) = paint(t, 90) # bright black — dimmer than dim() on most themes
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
# --- output levels --------------------------------------------------------
|
|
35
|
+
#
|
|
36
|
+
# phase ● a major phase boundary (build / sign / notarize)
|
|
37
|
+
# step → a top-level action inside a phase
|
|
38
|
+
# substep ▸ a sub-action under a step (indented)
|
|
39
|
+
# detail · raw-ish detail under a substep (indented + dim)
|
|
40
|
+
#
|
|
41
|
+
# Every level has a *_line string builder (used by LogFilter, which returns
|
|
42
|
+
# lines for someone else to print) and a printer that puts it. ok / success /
|
|
43
|
+
# warn / bad / die! carry outcomes at any level.
|
|
44
|
+
|
|
45
|
+
def phase_line(msg) = "\n#{bold(cyan("●"))} #{bold(msg)}"
|
|
46
|
+
def step_line(msg) = "#{cyan("→")} #{msg}"
|
|
47
|
+
def substep_line(msg) = " #{cyan("▸")} #{msg}"
|
|
48
|
+
def detail_line(msg) = " #{gray("·")} #{gray(msg)}"
|
|
49
|
+
def ok_line(msg) = "#{green("✓")} #{msg}"
|
|
50
|
+
def bad_line(msg) = "#{red("✗")} #{msg}"
|
|
51
|
+
def warn_line(msg) = "#{yellow("!")} #{msg}"
|
|
52
|
+
def success_line(msg) = "#{green("✓")} #{bold(msg)}"
|
|
53
|
+
|
|
54
|
+
def phase(msg) = puts(phase_line(msg))
|
|
55
|
+
def step(msg) = puts(step_line(msg))
|
|
56
|
+
def substep(msg) = puts(substep_line(msg))
|
|
57
|
+
def detail(msg) = puts(detail_line(msg))
|
|
58
|
+
def ok(msg) = puts(ok_line(msg))
|
|
59
|
+
def bad(msg) = puts(bad_line(msg))
|
|
60
|
+
def warn(msg) = puts(warn_line(msg))
|
|
61
|
+
def success(msg) = puts(success_line(msg))
|
|
62
|
+
|
|
63
|
+
# An indented, dim outcome under a substep — e.g. "accepted", "valid on disk".
|
|
64
|
+
def note_line(msg, marker: "·", color: :gray)
|
|
65
|
+
tint = respond_to?(color) ? method(color) : method(:gray)
|
|
66
|
+
" #{tint.call(marker)} #{tint.call(msg)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def note(msg, marker: "·", color: :gray) = puts(note_line(msg, marker: marker, color: color))
|
|
32
70
|
|
|
33
71
|
def die!(msg)
|
|
34
72
|
abort "#{red("✗")} #{red(msg)}"
|
|
35
73
|
end
|
|
74
|
+
|
|
75
|
+
# A yes/no fact, colored by truthiness (for signing receipts).
|
|
76
|
+
def yn(bool) = bool ? green("yes") : red("no")
|
|
77
|
+
|
|
78
|
+
# --- path shortening ------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
# Collapse the noisy machine paths that external tools echo (codesign,
|
|
81
|
+
# notarytool, stapler) down to something a human can scan. In particular the
|
|
82
|
+
# runner works out of a deep `/var/folders/…/every-runner…/src/` sandbox — we
|
|
83
|
+
# strip that prefix so only the meaningful `dist/App.app` tail remains. Then
|
|
84
|
+
# $HOME → ~ and the cwd → a leading (dropped) prefix.
|
|
85
|
+
def short_path(str)
|
|
86
|
+
s = str.to_s.dup
|
|
87
|
+
# Runner/local temp sandboxes: keep only the part after the extracted src/.
|
|
88
|
+
s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/every-[a-z]+[0-9A-Za-z_-]+/src/}, "")
|
|
89
|
+
# Any other everywhere-* scratch dir: keep the basename tail.
|
|
90
|
+
s = s.gsub(%r{/(?:private/)?(?:var|tmp)/[^\s"']*?/everywhere-[a-z]+[0-9A-Za-z_.-]*/}, "")
|
|
91
|
+
home = ENV["HOME"]
|
|
92
|
+
s = s.gsub("#{home}/", "~/") if home && !home.empty?
|
|
93
|
+
s
|
|
94
|
+
end
|
|
36
95
|
end
|
|
37
96
|
end
|
data/lib/everywhere/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_everywhere
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrea Fomera
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- lib/everywhere/icon.rb
|
|
70
70
|
- lib/everywhere/ignore.rb
|
|
71
71
|
- lib/everywhere/javascript/bridge.js
|
|
72
|
+
- lib/everywhere/log_filter.rb
|
|
72
73
|
- lib/everywhere/paths.rb
|
|
73
74
|
- lib/everywhere/platform/client.rb
|
|
74
75
|
- lib/everywhere/platform/credentials.rb
|