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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/exe/every +6 -0
  3. data/exe/rbe +6 -0
  4. data/lib/everywhere/boot.rb +138 -0
  5. data/lib/everywhere/cli.rb +44 -0
  6. data/lib/everywhere/commands/build.rb +267 -0
  7. data/lib/everywhere/commands/dev.rb +61 -0
  8. data/lib/everywhere/commands/doctor.rb +54 -0
  9. data/lib/everywhere/commands/install.rb +264 -0
  10. data/lib/everywhere/commands/platform/auth_status.rb +42 -0
  11. data/lib/everywhere/commands/platform/build.rb +181 -0
  12. data/lib/everywhere/commands/platform/login.rb +81 -0
  13. data/lib/everywhere/commands/platform/logout.rb +34 -0
  14. data/lib/everywhere/commands/platform/runner.rb +224 -0
  15. data/lib/everywhere/commands/release.rb +142 -0
  16. data/lib/everywhere/config.rb +188 -0
  17. data/lib/everywhere/database.rb +89 -0
  18. data/lib/everywhere/framework.rb +197 -0
  19. data/lib/everywhere/ignore.rb +85 -0
  20. data/lib/everywhere/javascript/bridge.js +174 -0
  21. data/lib/everywhere/paths.rb +35 -0
  22. data/lib/everywhere/platform/client.rb +125 -0
  23. data/lib/everywhere/platform/credentials.rb +71 -0
  24. data/lib/everywhere/platform/snapshot.rb +48 -0
  25. data/lib/everywhere/png.rb +110 -0
  26. data/lib/everywhere/receipt.rb +185 -0
  27. data/lib/everywhere/shellout.rb +46 -0
  28. data/lib/everywhere/ui.rb +37 -0
  29. data/lib/everywhere/version.rb +11 -0
  30. data/lib/everywhere.rb +50 -0
  31. data/lib/ruby_everywhere.rb +2 -3
  32. data/support/github/build.yml +85 -0
  33. data/support/macos/entitlements.plist +23 -0
  34. data/support/macos/notarize.sh +123 -0
  35. data/support/shell/splash/index.html +40 -0
  36. data/support/shell/src-tauri/Cargo.lock +5446 -0
  37. data/support/shell/src-tauri/Cargo.toml +19 -0
  38. data/support/shell/src-tauri/build.rs +3 -0
  39. data/support/shell/src-tauri/capabilities/default.json +22 -0
  40. data/support/shell/src-tauri/gen/schemas/acl-manifests.json +1 -0
  41. data/support/shell/src-tauri/gen/schemas/capabilities.json +1 -0
  42. data/support/shell/src-tauri/gen/schemas/desktop-schema.json +2634 -0
  43. data/support/shell/src-tauri/gen/schemas/macOS-schema.json +2634 -0
  44. data/support/shell/src-tauri/icons/icon.png +0 -0
  45. data/support/shell/src-tauri/src/main.rs +723 -0
  46. data/support/shell/src-tauri/tauri.conf.json +20 -0
  47. metadata +80 -7
  48. 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
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module RubyEverywhere
4
- VERSION = "0.0.1"
5
- end
3
+ # Conventional require path for the ruby_everywhere gem.
4
+ require_relative "everywhere"
@@ -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>