ruby_everywhere 0.0.1 → 0.1.1
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 +267 -0
- data/lib/everywhere/commands/dev.rb +61 -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 +142 -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/paths.rb +35 -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
- data/support/shell/splash/index.html +40 -0
- data/support/shell/src-tauri/Cargo.lock +5446 -0
- data/support/shell/src-tauri/Cargo.toml +19 -0
- data/support/shell/src-tauri/build.rs +3 -0
- data/support/shell/src-tauri/capabilities/default.json +22 -0
- data/support/shell/src-tauri/gen/schemas/acl-manifests.json +1 -0
- data/support/shell/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/shell/src-tauri/gen/schemas/desktop-schema.json +2634 -0
- data/support/shell/src-tauri/gen/schemas/macOS-schema.json +2634 -0
- data/support/shell/src-tauri/icons/icon.png +0 -0
- data/support/shell/src-tauri/src/main.rs +723 -0
- data/support/shell/src-tauri/tauri.conf.json +20 -0
- metadata +80 -7
- data/README.md +0 -11
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems/package"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "digest"
|
|
6
|
+
require_relative "../ignore"
|
|
7
|
+
|
|
8
|
+
module Everywhere
|
|
9
|
+
module Platform
|
|
10
|
+
# Packs the app's source into a .tar.gz for a Platform build, honoring
|
|
11
|
+
# .everywhereignore (so dist/, node_modules, .git, secrets never ship). Pure
|
|
12
|
+
# Ruby — no `tar` dependency — and streams file-by-file so a large tree
|
|
13
|
+
# doesn't blow up memory.
|
|
14
|
+
module Snapshot
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Writes root's non-ignored files to out_path (a .tar.gz). Returns the file
|
|
18
|
+
# count.
|
|
19
|
+
def create(root, out_path)
|
|
20
|
+
files = Everywhere::Ignore.for(root).files(root)
|
|
21
|
+
File.open(out_path, "wb") do |dest|
|
|
22
|
+
Zlib::GzipWriter.wrap(dest) do |gz|
|
|
23
|
+
Gem::Package::TarWriter.new(gz) do |tar|
|
|
24
|
+
files.each { |rel| add(tar, root, rel) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
files.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# base64 MD5 of a file (what Active Storage's direct upload verifies),
|
|
32
|
+
# computed in chunks so large snapshots don't load into memory.
|
|
33
|
+
def md5_base64(path)
|
|
34
|
+
digest = Digest::MD5.new
|
|
35
|
+
File.open(path, "rb") { |f| digest << f.read(1 << 20) until f.eof? }
|
|
36
|
+
digest.base64digest
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add(tar, root, rel)
|
|
40
|
+
abs = File.join(root, rel)
|
|
41
|
+
stat = File.stat(abs)
|
|
42
|
+
tar.add_file_simple(rel, stat.mode & 0o777, stat.size) do |io|
|
|
43
|
+
File.open(abs, "rb") { |f| IO.copy_stream(f, io) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -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.1"
|
|
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>
|