ruby_everywhere 0.0.1 → 0.1.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 +6 -0
- data/exe/rbe +6 -0
- data/lib/everywhere/boot.rb +138 -0
- data/lib/everywhere/cli.rb +44 -0
- data/lib/everywhere/commands/build.rb +272 -0
- data/lib/everywhere/commands/dev.rb +66 -0
- data/lib/everywhere/commands/doctor.rb +54 -0
- data/lib/everywhere/commands/install.rb +264 -0
- data/lib/everywhere/commands/platform/auth_status.rb +42 -0
- data/lib/everywhere/commands/platform/build.rb +181 -0
- data/lib/everywhere/commands/platform/login.rb +81 -0
- data/lib/everywhere/commands/platform/logout.rb +34 -0
- data/lib/everywhere/commands/platform/runner.rb +224 -0
- data/lib/everywhere/commands/release.rb +143 -0
- data/lib/everywhere/config.rb +188 -0
- data/lib/everywhere/database.rb +89 -0
- data/lib/everywhere/framework.rb +197 -0
- data/lib/everywhere/ignore.rb +85 -0
- data/lib/everywhere/javascript/bridge.js +174 -0
- data/lib/everywhere/platform/client.rb +125 -0
- data/lib/everywhere/platform/credentials.rb +71 -0
- data/lib/everywhere/platform/snapshot.rb +48 -0
- data/lib/everywhere/png.rb +110 -0
- data/lib/everywhere/receipt.rb +185 -0
- data/lib/everywhere/shellout.rb +46 -0
- data/lib/everywhere/ui.rb +37 -0
- data/lib/everywhere/version.rb +11 -0
- data/lib/everywhere.rb +50 -0
- data/lib/ruby_everywhere.rb +2 -3
- data/support/github/build.yml +85 -0
- data/support/macos/entitlements.plist +23 -0
- data/support/macos/notarize.sh +123 -0
- metadata +67 -7
- data/README.md +0 -11
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# Minimal PNG surgery: Tauri requires RGBA icons, users hand us whatever
|
|
7
|
+
# their design tool exported. Converts 8-bit RGB (color type 2) to RGBA
|
|
8
|
+
# (color type 6); passes RGBA through untouched. No image libraries needed.
|
|
9
|
+
module PNG
|
|
10
|
+
SIGNATURE = "\x89PNG\r\n\x1a\n".b
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Writes an RGBA copy of src to dst. Returns true on success, false for
|
|
15
|
+
# PNG flavors we don't handle (palette, 16-bit, interlaced...).
|
|
16
|
+
def ensure_rgba(src, dst)
|
|
17
|
+
data = File.binread(src)
|
|
18
|
+
return false unless data.start_with?(SIGNATURE)
|
|
19
|
+
|
|
20
|
+
chunks = read_chunks(data)
|
|
21
|
+
ihdr = chunks.find { |type, _| type == "IHDR" }&.last or return false
|
|
22
|
+
width, height, depth, color, _comp, _filter, interlace = ihdr.unpack("NNC5")
|
|
23
|
+
return false unless depth == 8 && interlace.zero?
|
|
24
|
+
|
|
25
|
+
if color == 6 # already RGBA
|
|
26
|
+
FileUtils.cp(src, dst)
|
|
27
|
+
return true
|
|
28
|
+
end
|
|
29
|
+
return false unless color == 2 # 8-bit RGB
|
|
30
|
+
|
|
31
|
+
raw = Zlib::Inflate.inflate(chunks.select { |t, _| t == "IDAT" }.map(&:last).join)
|
|
32
|
+
rgba = add_alpha(raw, width, height)
|
|
33
|
+
|
|
34
|
+
out = SIGNATURE.dup
|
|
35
|
+
out << chunk("IHDR", [width, height, 8, 6, 0, 0, 0].pack("NNC5"))
|
|
36
|
+
out << chunk("IDAT", Zlib::Deflate.deflate(rgba))
|
|
37
|
+
out << chunk("IEND", "")
|
|
38
|
+
File.binwrite(dst, out)
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_chunks(data)
|
|
43
|
+
chunks = []
|
|
44
|
+
pos = 8
|
|
45
|
+
while pos < data.bytesize
|
|
46
|
+
length = data[pos, 4].unpack1("N")
|
|
47
|
+
type = data[pos + 4, 4]
|
|
48
|
+
chunks << [type, data[pos + 8, length]]
|
|
49
|
+
pos += 12 + length
|
|
50
|
+
break if type == "IEND"
|
|
51
|
+
end
|
|
52
|
+
chunks
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Unfilter RGB scanlines, emit unfiltered (filter 0) RGBA scanlines.
|
|
56
|
+
def add_alpha(raw, width, height)
|
|
57
|
+
bpp = 3
|
|
58
|
+
stride = width * bpp
|
|
59
|
+
prev = "\0".b * stride
|
|
60
|
+
out = +"".b
|
|
61
|
+
|
|
62
|
+
height.times do |y|
|
|
63
|
+
filter = raw.getbyte(y * (stride + 1))
|
|
64
|
+
line = raw.byteslice(y * (stride + 1) + 1, stride).dup
|
|
65
|
+
unfilter!(line, prev, filter, bpp)
|
|
66
|
+
|
|
67
|
+
out << "\0"
|
|
68
|
+
width.times do |x|
|
|
69
|
+
out << line.byteslice(x * bpp, bpp) << "\xFF".b
|
|
70
|
+
end
|
|
71
|
+
prev = line
|
|
72
|
+
end
|
|
73
|
+
out
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unfilter!(line, prev, filter, bpp)
|
|
77
|
+
stride = line.bytesize
|
|
78
|
+
case filter
|
|
79
|
+
when 0 # None
|
|
80
|
+
when 1 # Sub
|
|
81
|
+
bpp.upto(stride - 1) { |i| line.setbyte(i, (line.getbyte(i) + line.getbyte(i - bpp)) & 0xFF) }
|
|
82
|
+
when 2 # Up
|
|
83
|
+
0.upto(stride - 1) { |i| line.setbyte(i, (line.getbyte(i) + prev.getbyte(i)) & 0xFF) }
|
|
84
|
+
when 3 # Average
|
|
85
|
+
0.upto(stride - 1) do |i|
|
|
86
|
+
left = i >= bpp ? line.getbyte(i - bpp) : 0
|
|
87
|
+
line.setbyte(i, (line.getbyte(i) + ((left + prev.getbyte(i)) >> 1)) & 0xFF)
|
|
88
|
+
end
|
|
89
|
+
when 4 # Paeth
|
|
90
|
+
0.upto(stride - 1) do |i|
|
|
91
|
+
left = i >= bpp ? line.getbyte(i - bpp) : 0
|
|
92
|
+
up = prev.getbyte(i)
|
|
93
|
+
up_left = i >= bpp ? prev.getbyte(i - bpp) : 0
|
|
94
|
+
p = left + up - up_left
|
|
95
|
+
pa = (p - left).abs
|
|
96
|
+
pb = (p - up).abs
|
|
97
|
+
pc = (p - up_left).abs
|
|
98
|
+
predictor = pa <= pb && pa <= pc ? left : (pb <= pc ? up : up_left)
|
|
99
|
+
line.setbyte(i, (line.getbyte(i) + predictor) & 0xFF)
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
raise Error, "unknown PNG filter #{filter}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def chunk(type, payload)
|
|
107
|
+
[payload.bytesize].pack("N") + type + payload + [Zlib.crc32(type + payload)].pack("N")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "version"
|
|
6
|
+
require_relative "shellout"
|
|
7
|
+
require_relative "ignore"
|
|
8
|
+
|
|
9
|
+
module Everywhere
|
|
10
|
+
# Builds the machine-readable build receipt (release.json) — the same shape
|
|
11
|
+
# emitted by local, CI, and Platform builds. See platform/docs/build-engine.md
|
|
12
|
+
# §7.
|
|
13
|
+
#
|
|
14
|
+
# Two layers:
|
|
15
|
+
# * the *resolved manifest* — the deterministic build request (app,
|
|
16
|
+
# framework, ruby, toolchain versions, source checksums, target,
|
|
17
|
+
# permissions). Everything here is knowable BEFORE the build runs and
|
|
18
|
+
# carries no volatile fields, so the same source + toolchain hashes
|
|
19
|
+
# identically (manifest_checksum).
|
|
20
|
+
# * the *artifact* record — filename, checksum, size, and the generic
|
|
21
|
+
# `signing` block, layered on once a build has produced something.
|
|
22
|
+
class Receipt
|
|
23
|
+
SCHEMA = 1
|
|
24
|
+
|
|
25
|
+
def initialize(root:, config:, framework:, ruby:, target:, channel:, shell_dir: nil)
|
|
26
|
+
@root = File.expand_path(root)
|
|
27
|
+
@config = config
|
|
28
|
+
@framework = framework
|
|
29
|
+
@ruby = ruby
|
|
30
|
+
@target = parse_target(target, channel)
|
|
31
|
+
@shell_dir = shell_dir
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The deterministic build request.
|
|
35
|
+
def manifest
|
|
36
|
+
{
|
|
37
|
+
"app" => {
|
|
38
|
+
"name" => @config.name,
|
|
39
|
+
"bundle_id" => @config.bundle_id,
|
|
40
|
+
"version" => @config.version
|
|
41
|
+
},
|
|
42
|
+
"framework" => framework_info,
|
|
43
|
+
"ruby" => @ruby,
|
|
44
|
+
"versions" => versions,
|
|
45
|
+
"source" => {
|
|
46
|
+
"checksum" => source_checksum,
|
|
47
|
+
"lockfile_checksum" => lockfile_checksum
|
|
48
|
+
},
|
|
49
|
+
"target" => @target,
|
|
50
|
+
"permissions" => @config.permissions
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# sha256 over the canonical (sorted-key) JSON of the manifest. Ties a receipt
|
|
55
|
+
# to the exact request that produced it.
|
|
56
|
+
def manifest_checksum
|
|
57
|
+
"sha256:#{Digest::SHA256.hexdigest(canonical_json(manifest))}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The full receipt: manifest + artifact record. `signing` is the generic
|
|
61
|
+
# block from the spec — common fields + a platform-specific bag.
|
|
62
|
+
def to_h(artifact:, signing:, build_id: nil, logs_url: nil)
|
|
63
|
+
{
|
|
64
|
+
"schema" => SCHEMA,
|
|
65
|
+
"build_id" => build_id,
|
|
66
|
+
"manifest_checksum" => manifest_checksum
|
|
67
|
+
}.merge(manifest).merge(
|
|
68
|
+
"artifact" => artifact_record(artifact).merge("signing" => signing),
|
|
69
|
+
"logs_url" => logs_url
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def write(path, **kwargs)
|
|
74
|
+
File.write(path, "#{JSON.pretty_generate(to_h(**kwargs))}\n")
|
|
75
|
+
path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def parse_target(target, channel)
|
|
81
|
+
os, arch = target.to_s.split("-", 2)
|
|
82
|
+
{ "os" => os, "arch" => arch, "distribution_channel" => channel }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Remote-mode apps package no local framework/runtime.
|
|
86
|
+
def framework_info
|
|
87
|
+
return { "name" => "remote", "version" => nil } unless @framework
|
|
88
|
+
|
|
89
|
+
{ "name" => @framework.name, "version" => lockfile_gem_version(@framework.name) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def artifact_record(path)
|
|
93
|
+
{
|
|
94
|
+
"filename" => File.basename(path),
|
|
95
|
+
"checksum" => "sha256:#{Digest::SHA256.file(path).hexdigest}",
|
|
96
|
+
"size_bytes" => File.size(path)
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ---- versions ------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def versions
|
|
103
|
+
{
|
|
104
|
+
"cli" => Everywhere::VERSION,
|
|
105
|
+
"bridge" => bridge_version,
|
|
106
|
+
"shell" => shell_version,
|
|
107
|
+
"tebako" => tebako_version
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def bridge_version
|
|
112
|
+
# Package installs: read the version straight from package.json.
|
|
113
|
+
[
|
|
114
|
+
File.join(@root, "node_modules", "@rubyeverywhere", "bridge", "package.json"),
|
|
115
|
+
File.join(@root, "vendor", "javascript", "@rubyeverywhere", "bridge", "package.json")
|
|
116
|
+
].each { |p| (v = json_version(p)) and return v }
|
|
117
|
+
|
|
118
|
+
# Single-file vendored install: read a `// version: x.y.z` header marker
|
|
119
|
+
# if `every install` stamped one (best-effort until it does).
|
|
120
|
+
flat = File.join(@root, "vendor", "javascript", "@rubyeverywhere--bridge.js")
|
|
121
|
+
File.exist?(flat) ? File.foreach(flat).first(6).join[/version:\s*([\d.]+)/i, 1] : nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def shell_version
|
|
125
|
+
return nil unless @shell_dir
|
|
126
|
+
|
|
127
|
+
json_version(File.join(@shell_dir, "tauri.conf.json")) ||
|
|
128
|
+
json_version(File.join(@shell_dir, "..", "tauri.conf.json"))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The rbe-tebako fork prints its banner ("...version 0.15.0") on every
|
|
132
|
+
# invocation and exits non-zero for unknown subcommands, so parse the output
|
|
133
|
+
# rather than gate on the exit status.
|
|
134
|
+
def tebako_version
|
|
135
|
+
out, = Shellout.capture("rbe-tebako", "--version")
|
|
136
|
+
out[/version\s+([\d.]+)/i, 1]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ---- checksums -----------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def lockfile_checksum
|
|
142
|
+
lock = File.join(@root, "Gemfile.lock")
|
|
143
|
+
File.exist?(lock) ? "sha256:#{Digest::SHA256.file(lock).hexdigest}" : nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def lockfile_gem_version(gem_name)
|
|
147
|
+
lock = File.join(@root, "Gemfile.lock")
|
|
148
|
+
return nil unless File.exist?(lock)
|
|
149
|
+
|
|
150
|
+
File.read(lock)[/^\s{4}#{Regexp.escape(gem_name)} \(([^)]+)\)/, 1]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Deterministic digest of the app's source: for every non-ignored file,
|
|
154
|
+
# "relpath\0<sha256>\n", sorted, then hashed. Order-stable across machines.
|
|
155
|
+
# Uses the SAME ignore rules as the (future) snapshot, so the checksum
|
|
156
|
+
# describes exactly what ships.
|
|
157
|
+
def source_checksum
|
|
158
|
+
digest = Digest::SHA256.new
|
|
159
|
+
Ignore.for(@root).files(@root).each do |rel|
|
|
160
|
+
digest << rel << "\0" << Digest::SHA256.file(File.join(@root, rel)).hexdigest << "\n"
|
|
161
|
+
end
|
|
162
|
+
"sha256:#{digest.hexdigest}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# ---- helpers -------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def json_version(path)
|
|
168
|
+
return nil unless File.exist?(path)
|
|
169
|
+
|
|
170
|
+
JSON.parse(File.read(path))["version"]
|
|
171
|
+
rescue JSON::ParserError
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def canonical_json(obj) = JSON.generate(sort_deep(obj))
|
|
176
|
+
|
|
177
|
+
def sort_deep(obj)
|
|
178
|
+
case obj
|
|
179
|
+
when Hash then obj.sort_by { |k, _| k.to_s }.to_h.transform_values { |v| sort_deep(v) }
|
|
180
|
+
when Array then obj.map { |v| sort_deep(v) }
|
|
181
|
+
else obj
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ui"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# All external commands (tebako, cargo, the app's own bin/dev) run outside
|
|
7
|
+
# this gem's bundle — they have their own gem environments.
|
|
8
|
+
module Shellout
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def unbundled(&block)
|
|
12
|
+
defined?(Bundler) ? Bundler.with_unbundled_env(&block) : yield
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run!(env, *cmd, chdir: Dir.pwd)
|
|
16
|
+
success = unbundled { system(env, *cmd, chdir: chdir) }
|
|
17
|
+
UI.die!("command failed: #{cmd.join(" ")}") unless success
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run?(*cmd)
|
|
21
|
+
unbundled { system(*cmd) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Run a command and capture its combined stdout+stderr. Returns
|
|
25
|
+
# [output_string, Process::Status]. For probing tool output (codesign,
|
|
26
|
+
# stapler, notarytool) rather than driving a build.
|
|
27
|
+
def capture(*cmd)
|
|
28
|
+
require "open3"
|
|
29
|
+
unbundled { Open3.capture2e(*cmd) }
|
|
30
|
+
rescue Errno::ENOENT
|
|
31
|
+
["", nil]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Run a command streaming to the console while also capturing to a log file.
|
|
35
|
+
def run_logged!(env, cmd, log:)
|
|
36
|
+
require "shellwords"
|
|
37
|
+
line = "#{cmd.shelljoin} 2>&1 | tee #{log.shellescape}"
|
|
38
|
+
success = unbundled { system(env, "/bin/bash", "-o", "pipefail", "-c", line) }
|
|
39
|
+
UI.die!("command failed: #{cmd.join(" ")} (full log: #{log})") unless success
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def spawn(env, cmd, chdir:)
|
|
43
|
+
unbundled { Process.spawn(env, cmd, chdir: chdir) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# Terminal output helpers. Colors follow the informal standard:
|
|
5
|
+
# on when stdout is a TTY or CLICOLOR_FORCE=1, always off when NO_COLOR is set.
|
|
6
|
+
module UI
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def color?
|
|
10
|
+
return false if ENV["NO_COLOR"]
|
|
11
|
+
return true if ENV["CLICOLOR_FORCE"] == "1"
|
|
12
|
+
|
|
13
|
+
$stdout.tty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def paint(text, *codes)
|
|
17
|
+
color? ? "\e[#{codes.join(";")}m#{text}\e[0m" : text.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bold(t) = paint(t, 1)
|
|
21
|
+
def dim(t) = paint(t, 2)
|
|
22
|
+
def red(t) = paint(t, 31)
|
|
23
|
+
def green(t) = paint(t, 32)
|
|
24
|
+
def yellow(t) = paint(t, 33)
|
|
25
|
+
def cyan(t) = paint(t, 36)
|
|
26
|
+
|
|
27
|
+
def step(msg) = puts("#{cyan("→")} #{msg}")
|
|
28
|
+
def ok(msg) = puts("#{green("✓")} #{msg}")
|
|
29
|
+
def bad(msg) = puts("#{red("✗")} #{msg}")
|
|
30
|
+
def warn(msg) = puts("#{yellow("!")} #{msg}")
|
|
31
|
+
def success(msg) = puts("#{green("✓")} #{bold(msg)}")
|
|
32
|
+
|
|
33
|
+
def die!(msg)
|
|
34
|
+
abort "#{red("✗")} #{red(msg)}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
VERSION = "0.1.0"
|
|
5
|
+
|
|
6
|
+
# Version of the vendored @rubyeverywhere/bridge JS this gem ships. Tracks
|
|
7
|
+
# bridge/package.json — bump it whenever lib/everywhere/javascript/bridge.js is
|
|
8
|
+
# refreshed. `every install` stamps it into the vendored file so the build
|
|
9
|
+
# receipt can record which bridge shipped; it versions independently of the CLI.
|
|
10
|
+
BRIDGE_VERSION = "0.1.0"
|
|
11
|
+
end
|
data/lib/everywhere.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Runtime entry point: what an app gets from `require "ruby_everywhere"`.
|
|
4
|
+
# The CLI lives in everywhere/cli and is only loaded by the every/rbe executables.
|
|
5
|
+
require_relative "everywhere/version"
|
|
6
|
+
require_relative "everywhere/config"
|
|
7
|
+
require_relative "everywhere/framework"
|
|
8
|
+
require_relative "everywhere/database"
|
|
9
|
+
require_relative "everywhere/boot"
|
|
10
|
+
|
|
11
|
+
module Everywhere
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
# App-facing runtime configuration. Apps register overrides at load time:
|
|
15
|
+
#
|
|
16
|
+
# # config/initializers/everywhere.rb (Rails) or anywhere loaded by config.ru
|
|
17
|
+
# Everywhere.configure do |c|
|
|
18
|
+
# c.database { MyMigrator.run! } # override the default ORM-driven prep
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# Read back by the boot path (Everywhere::Database, etc.).
|
|
22
|
+
class Configuration
|
|
23
|
+
# A callable that fully owns database preparation. When set, it replaces the
|
|
24
|
+
# ORM auto-detection in Everywhere::Database entirely.
|
|
25
|
+
attr_reader :database_hook
|
|
26
|
+
|
|
27
|
+
# An explicit Sequel database handle for frameworks (e.g. Hanami) where the
|
|
28
|
+
# connection lives inside a container rather than a top-level constant.
|
|
29
|
+
attr_accessor :sequel_db
|
|
30
|
+
|
|
31
|
+
def database(&block)
|
|
32
|
+
@database_hook = block
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def config
|
|
38
|
+
@config ||= Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configure
|
|
42
|
+
yield config
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Test/reset seam.
|
|
46
|
+
def reset_config!
|
|
47
|
+
@config = Configuration.new
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/ruby_everywhere.rb
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# RubyEverywhere Platform — reusable build workflow.
|
|
2
|
+
#
|
|
3
|
+
# Lives in the RunnerDispatch build repo (private, RubyEverywhere-owned). The
|
|
4
|
+
# Platform triggers it via workflow_dispatch (see app/models/runner_dispatch.rb);
|
|
5
|
+
# it runs one artifact on an ephemeral, single-use macOS runner and reports back
|
|
6
|
+
# through the Runner API. No customer source lives in this repo — it arrives as
|
|
7
|
+
# the snapshot the runner downloads.
|
|
8
|
+
#
|
|
9
|
+
# The heavy part is provisioning the toolchain (rbe-tebako fork + deps + Xcode
|
|
10
|
+
# shims). Cache it aggressively; a warm runner builds in minutes.
|
|
11
|
+
|
|
12
|
+
name: RubyEverywhere Platform Build
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
inputs:
|
|
17
|
+
artifact_id:
|
|
18
|
+
description: Artifact public id (art_…)
|
|
19
|
+
required: true
|
|
20
|
+
runner_token:
|
|
21
|
+
description: Per-artifact runner token
|
|
22
|
+
required: true
|
|
23
|
+
callback_base_url:
|
|
24
|
+
description: Platform base URL the runner reports back to
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
# One in-flight job per artifact.
|
|
28
|
+
concurrency:
|
|
29
|
+
group: build-${{ inputs.artifact_id }}
|
|
30
|
+
cancel-in-progress: false
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
build:
|
|
34
|
+
# macOS/iOS → macos; Android → ubuntu; Windows → windows (routed by the
|
|
35
|
+
# dispatcher today only macOS is live).
|
|
36
|
+
runs-on: macos-14 # Apple Silicon
|
|
37
|
+
timeout-minutes: 45
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- name: Set up Ruby (to run the every CLI itself)
|
|
41
|
+
uses: ruby/setup-ruby@v1
|
|
42
|
+
with:
|
|
43
|
+
ruby-version: "3.4"
|
|
44
|
+
|
|
45
|
+
- name: Provision the RubyEverywhere toolchain
|
|
46
|
+
run: |
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
# The signing engine needs the rbe-tebako fork + build deps. This is the
|
|
49
|
+
# slow step — restore it from cache in a real setup (actions/cache on
|
|
50
|
+
# ~/.tebako, ~/.tebako-shims, brew).
|
|
51
|
+
brew install bison
|
|
52
|
+
gem install ruby_everywhere # provides `every`
|
|
53
|
+
# gem install rbe-tebako # the fork (or build from source)
|
|
54
|
+
# bin/setup-xcode-shims # Xcode 26 CXX shim, rbconfig patch
|
|
55
|
+
every doctor || true # surfaces any missing toolchain bits
|
|
56
|
+
|
|
57
|
+
- name: Install Apple intermediate certificates
|
|
58
|
+
run: |
|
|
59
|
+
set -euo pipefail
|
|
60
|
+
# The delivered .p12 holds only the leaf; codesign needs the Developer
|
|
61
|
+
# ID intermediate to build a complete chain (a clean runner has none).
|
|
62
|
+
for cert in DeveloperIDG2CA AppleWWDRCAG3; do
|
|
63
|
+
curl -fsSL "https://www.apple.com/certificateauthority/$cert.cer" -o "$RUNNER_TEMP/$cert.cer"
|
|
64
|
+
sudo security import "$RUNNER_TEMP/$cert.cer" -k /Library/Keychains/System.keychain || true
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
- name: Provision the generic Tauri shell
|
|
68
|
+
run: |
|
|
69
|
+
set -euo pipefail
|
|
70
|
+
# The shell (src-tauri) is generic — same for every app. Pull the
|
|
71
|
+
# pinned prebuilt shell for this platform.
|
|
72
|
+
echo "SHELL_DIR=$RUNNER_TEMP/shell/src-tauri" >> "$GITHUB_ENV"
|
|
73
|
+
# gh release download --repo rubyeverywhere/shell --pattern 'src-tauri-*' ...
|
|
74
|
+
# OR: git clone --depth 1 https://github.com/rubyeverywhere/shell "$RUNNER_TEMP/shell"
|
|
75
|
+
|
|
76
|
+
- name: Run the build
|
|
77
|
+
run: |
|
|
78
|
+
every platform runner \
|
|
79
|
+
--artifact "${{ inputs.artifact_id }}" \
|
|
80
|
+
--token "${{ inputs.runner_token }}" \
|
|
81
|
+
--url "${{ inputs.callback_base_url }}" \
|
|
82
|
+
--shell-dir "$SHELL_DIR"
|
|
83
|
+
# Signing credentials are pulled JIT from the Platform into a throwaway
|
|
84
|
+
# keychain by the runner itself — they are never GitHub secrets, and the
|
|
85
|
+
# ephemeral runner is destroyed after this job.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<!--
|
|
6
|
+
Hardened-runtime entitlements required to notarize a tebako-packed Ruby app.
|
|
7
|
+
|
|
8
|
+
A tebako binary embeds Ruby + all gems in a DwarFS memfs and, at runtime,
|
|
9
|
+
dlopen()s native gem extensions (.bundle) straight out of that memfs. Those
|
|
10
|
+
.bundle files are NOT signed with our Developer ID, so the hardened runtime's
|
|
11
|
+
library validation would reject them — hence disable-library-validation.
|
|
12
|
+
|
|
13
|
+
Ruby 4's YJIT/ZJIT allocate + execute JIT code, which the hardened runtime
|
|
14
|
+
blocks unless we allow JIT and unsigned executable memory.
|
|
15
|
+
-->
|
|
16
|
+
<key>com.apple.security.cs.disable-library-validation</key>
|
|
17
|
+
<true/>
|
|
18
|
+
<key>com.apple.security.cs.allow-jit</key>
|
|
19
|
+
<true/>
|
|
20
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
21
|
+
<true/>
|
|
22
|
+
</dict>
|
|
23
|
+
</plist>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Sign (Developer ID + hardened runtime), notarize, and staple a .app bundle
|
|
4
|
+
# so it launches without Gatekeeper warnings on other Macs.
|
|
5
|
+
#
|
|
6
|
+
# Secrets never live in this script. Provide them via the environment — the
|
|
7
|
+
# easiest way is to source your release env first:
|
|
8
|
+
#
|
|
9
|
+
# source ~/Projects/atlastools.dev/atlas-workspaces/.env.release
|
|
10
|
+
# cli/support/macos/notarize.sh "example/dist/Example Notes.app"
|
|
11
|
+
#
|
|
12
|
+
# Required env vars (these match Tauri's names, so your existing .env works):
|
|
13
|
+
# APPLE_SIGNING_IDENTITY e.g. "Developer ID Application: Your Name (TEAMID)"
|
|
14
|
+
# APPLE_ID your Apple ID email
|
|
15
|
+
# APPLE_PASSWORD app-specific password (NOT your Apple account password)
|
|
16
|
+
# APPLE_TEAM_ID your 10-char team id
|
|
17
|
+
#
|
|
18
|
+
# Optional:
|
|
19
|
+
# NOTARY_PROFILE if set, uses `--keychain-profile "$NOTARY_PROFILE"`
|
|
20
|
+
# instead of APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID.
|
|
21
|
+
# Create it once with:
|
|
22
|
+
# xcrun notarytool store-credentials "$NOTARY_PROFILE" \
|
|
23
|
+
# --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" \
|
|
24
|
+
# --password "$APPLE_PASSWORD"
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
APP="${1:?usage: notarize.sh /path/to/App.app}"
|
|
29
|
+
[ -d "$APP" ] || { echo "not a .app bundle: $APP" >&2; exit 1; }
|
|
30
|
+
|
|
31
|
+
# Load secrets from a dotenv-style file when ENV_FILE is set. This parses
|
|
32
|
+
# KEY=VALUE line-by-line and exports each var, so it handles unquoted values
|
|
33
|
+
# containing spaces/parens (e.g. APPLE_SIGNING_IDENTITY=Developer ID
|
|
34
|
+
# Application: Name (TEAM)) that plain `source .env.release` cannot.
|
|
35
|
+
#
|
|
36
|
+
# ENV_FILE=.env.release cli/support/macos/notarize.sh "App.app"
|
|
37
|
+
if [ -n "${ENV_FILE:-}" ]; then
|
|
38
|
+
[ -f "$ENV_FILE" ] || { echo "ENV_FILE not found: $ENV_FILE" >&2; exit 1; }
|
|
39
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
40
|
+
line="${line%$'\r'}" # strip trailing CR (CRLF files)
|
|
41
|
+
case "$line" in ''|'#'*) continue ;; esac # skip blank / comment lines
|
|
42
|
+
[ "${line#*=}" = "$line" ] && continue # no '=' on the line -> skip
|
|
43
|
+
key="${line%%=*}"; val="${line#*=}"
|
|
44
|
+
key="${key#export }" # allow "export KEY=..."
|
|
45
|
+
key="${key#"${key%%[![:space:]]*}"}" # trim leading whitespace
|
|
46
|
+
key="${key%"${key##*[![:space:]]}"}" # trim trailing whitespace
|
|
47
|
+
# strip one layer of matching surrounding quotes from the value
|
|
48
|
+
if [ "${val#\"}" != "$val" ] && [ "${val%\"}" != "$val" ]; then val="${val#\"}"; val="${val%\"}"
|
|
49
|
+
elif [ "${val#\'}" != "$val" ] && [ "${val%\'}" != "$val" ]; then val="${val#\'}"; val="${val%\'}"
|
|
50
|
+
fi
|
|
51
|
+
export "$key=$val"
|
|
52
|
+
done < "$ENV_FILE"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
56
|
+
ENTITLEMENTS="$SCRIPT_DIR/entitlements.plist"
|
|
57
|
+
|
|
58
|
+
: "${APPLE_SIGNING_IDENTITY:?set APPLE_SIGNING_IDENTITY (source your .env.release)}"
|
|
59
|
+
|
|
60
|
+
# --- 1. Sign inside-out: nested Mach-O first, then the bundle --------------
|
|
61
|
+
echo "==> signing with: $APPLE_SIGNING_IDENTITY"
|
|
62
|
+
# Sign every executable/dylib under Contents/MacOS individually so nested
|
|
63
|
+
# helpers (app-server, the tebako binary) each carry the entitlements. The
|
|
64
|
+
# bundle sign at the end seals resources and the main executable.
|
|
65
|
+
while IFS= read -r -d '' bin; do
|
|
66
|
+
echo " sign $bin"
|
|
67
|
+
codesign --force --timestamp --options runtime \
|
|
68
|
+
--entitlements "$ENTITLEMENTS" \
|
|
69
|
+
--sign "$APPLE_SIGNING_IDENTITY" "$bin"
|
|
70
|
+
done < <(find "$APP/Contents/MacOS" -type f -print0)
|
|
71
|
+
|
|
72
|
+
echo " sign $APP"
|
|
73
|
+
codesign --force --timestamp --options runtime \
|
|
74
|
+
--entitlements "$ENTITLEMENTS" \
|
|
75
|
+
--sign "$APPLE_SIGNING_IDENTITY" "$APP"
|
|
76
|
+
|
|
77
|
+
echo "==> verifying signature"
|
|
78
|
+
codesign --verify --deep --strict --verbose=2 "$APP"
|
|
79
|
+
|
|
80
|
+
# --- 2. Notarize ----------------------------------------------------------
|
|
81
|
+
ZIP="${APP%.app}.zip"
|
|
82
|
+
echo "==> zipping for notarization: $ZIP"
|
|
83
|
+
rm -f "$ZIP"
|
|
84
|
+
ditto -c -k --keepParent "$APP" "$ZIP"
|
|
85
|
+
|
|
86
|
+
echo "==> submitting to Apple notary service (this can take a few minutes)"
|
|
87
|
+
if [ -n "${NOTARY_KEY:-}" ]; then
|
|
88
|
+
# App Store Connect API key (.p8) — how the Platform runner notarizes with a
|
|
89
|
+
# custodied key. NOTARY_KEY is a path to the .p8.
|
|
90
|
+
: "${NOTARY_KEY_ID:?set NOTARY_KEY_ID with NOTARY_KEY}"
|
|
91
|
+
: "${NOTARY_ISSUER:?set NOTARY_ISSUER with NOTARY_KEY}"
|
|
92
|
+
xcrun notarytool submit "$ZIP" \
|
|
93
|
+
--key "$NOTARY_KEY" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER" --wait
|
|
94
|
+
elif [ -n "${NOTARY_PROFILE:-}" ]; then
|
|
95
|
+
xcrun notarytool submit "$ZIP" --keychain-profile "$NOTARY_PROFILE" --wait
|
|
96
|
+
else
|
|
97
|
+
# Accept APPLE_APP_SPECIFIC_PASSWORD as an alias for APPLE_PASSWORD.
|
|
98
|
+
APPLE_PASSWORD="${APPLE_PASSWORD:-${APPLE_APP_SPECIFIC_PASSWORD:-}}"
|
|
99
|
+
: "${APPLE_ID:?set APPLE_ID or NOTARY_PROFILE}"
|
|
100
|
+
: "${APPLE_PASSWORD:?set APPLE_PASSWORD (or APPLE_APP_SPECIFIC_PASSWORD) or NOTARY_PROFILE}"
|
|
101
|
+
: "${APPLE_TEAM_ID:?set APPLE_TEAM_ID or NOTARY_PROFILE}"
|
|
102
|
+
xcrun notarytool submit "$ZIP" \
|
|
103
|
+
--apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD" \
|
|
104
|
+
--wait
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# --- 3. Staple + verify ---------------------------------------------------
|
|
108
|
+
echo "==> stapling ticket"
|
|
109
|
+
xcrun stapler staple "$APP"
|
|
110
|
+
xcrun stapler validate "$APP"
|
|
111
|
+
|
|
112
|
+
echo "==> Gatekeeper assessment"
|
|
113
|
+
spctl -a -vvv "$APP"
|
|
114
|
+
|
|
115
|
+
# --- 4. Re-zip the stapled app for distribution ---------------------------
|
|
116
|
+
echo "==> re-zipping stapled app for distribution"
|
|
117
|
+
rm -f "$ZIP"
|
|
118
|
+
ditto -c -k --keepParent "$APP" "$ZIP"
|
|
119
|
+
|
|
120
|
+
echo ""
|
|
121
|
+
echo "✅ Done. Send this to your friend: $ZIP"
|
|
122
|
+
echo " (Stapled, so it works even offline. Send the .zip, not the raw .app —"
|
|
123
|
+
echo " zipping preserves the signature; AirDrop/Finder-copy of a bare .app can strip it.)"
|