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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39645f3a9d06b112aacb1d2f6863914e355b027f73601de21a04fb2d45b0f1a9
4
- data.tar.gz: 87e978358814f098d3f1c596edc5d75b1f843608bde316fc0cebf159a57ffb5f
3
+ metadata.gz: 19938d7ab43425ebeb6a729c9dff31fa1c301aea0b72b08638483a1e2e1e5313
4
+ data.tar.gz: b493f9396d3b2f7b84d2b9434ab232ef21294df373b8c432d2c1aac811c9ac98
5
5
  SHA512:
6
- metadata.gz: bc57bb378e7b44fc08e5a2e5a5929a240ef6be7ac7bc506302a597aaa829c116fb8df391313dcb879824a420b0befeb6b72ee66f8993b1ab393fc274fc0ffcb4
7
- data.tar.gz: 1aae4125b5d23220f900c0650b88102f09b274213aea8c201e0fb435ebc8b38bb7bd3b4a546b58a805f92ee3533e6f7305ab53b5cfdd51a0fe80946485d72558
6
+ metadata.gz: fd4d9e03d266ee568170e60b663034c519db76941e2f91057dcc88cbb2250f71dae01668049a0af35edbd7464d468903c0027d97664da150b67529d62f7974a7
7
+ data.tar.gz: 6bb5a9afbe5d330f6a5bf7be303e728626b8df04476c9b5f3931ef0ed2d71ba9bf5507a1c54b99f0e11d7305c1acdbb2cb68f7bb43d7f46967dbf6746c5400e0
data/exe/every ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "everywhere/cli"
5
+
6
+ Dry::CLI.new(Everywhere::CLI).call
data/exe/rbe ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "everywhere/cli"
5
+
6
+ Dry::CLI.new(Everywhere::CLI).call
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "securerandom"
5
+
6
+ module Everywhere
7
+ # Runtime boot for the packaged desktop build. Called from the app's
8
+ # generated native_boot.rb stub (after the bundle is set up):
9
+ #
10
+ # Everywhere.boot!(root: __dir__)
11
+ #
12
+ # This core is framework-neutral. Its responsibilities:
13
+ # 1. Resolve a writable per-user app-data directory (packaged FS is read-only)
14
+ # 2. Generate a per-install SECRET_KEY_BASE on first boot
15
+ # 3. Copy static assets to a writable dir (the memfs truncates large files)
16
+ # 4. Watch the shell process and shut down if it dies
17
+ #
18
+ # Everything framework-specific — loading the app, preparing the database, and
19
+ # starting the server — is delegated to the detected Framework adapter, so
20
+ # Rails, Sinatra, and Hanami each own their own boot path.
21
+ #
22
+ # The desktop shell passes NATIVE_PORT and NATIVE_APPDATA; standalone runs
23
+ # (debugging) get a free port and a config-derived app-data dir.
24
+ class Boot
25
+ def self.call(root:)
26
+ new(root).call
27
+ end
28
+
29
+ attr_reader :root, :config
30
+
31
+ def initialize(root)
32
+ @root = File.expand_path(root)
33
+ @config = Config.load(@root)
34
+ end
35
+
36
+ def call
37
+ ENV["NATIVE_PACKAGED"] ||= "1"
38
+
39
+ framework = Framework.detect(root)
40
+ framework.prepare_runtime_env
41
+
42
+ prepare_app_data
43
+ ensure_secret_key_base
44
+ extract_public_dir
45
+
46
+ Dir.chdir(root)
47
+ watch_shell
48
+
49
+ port = ENV["NATIVE_PORT"] || free_port
50
+ puts "[everywhere] framework: #{framework.name}"
51
+ puts "[everywhere] app data: #{app_data}"
52
+ puts "[everywhere] app running at http://127.0.0.1:#{port}"
53
+
54
+ framework.boot!(port: port)
55
+ end
56
+
57
+ private
58
+
59
+ def app_data
60
+ @app_data ||= ENV["NATIVE_APPDATA"] || default_app_data
61
+ end
62
+
63
+ # Same layout the desktop shell uses: platform data dir + bundle_id.
64
+ def default_app_data
65
+ case RUBY_PLATFORM
66
+ when /darwin/
67
+ File.join(Dir.home, "Library", "Application Support", config.bundle_id)
68
+ when /mingw|mswin/
69
+ File.join(ENV.fetch("APPDATA"), config.bundle_id)
70
+ else
71
+ File.join(ENV.fetch("XDG_DATA_HOME", File.join(Dir.home, ".local", "share")),
72
+ config.bundle_id)
73
+ end
74
+ end
75
+
76
+ def prepare_app_data
77
+ storage = File.join(app_data, "storage")
78
+ FileUtils.mkdir_p(storage)
79
+ ENV["NATIVE_STORAGE_DIR"] = storage
80
+ end
81
+
82
+ # Files larger than ~64KB truncate when streamed from the tebako memfs
83
+ # (Rack::Files sends an empty body), so static assets are served from a
84
+ # real-disk copy in app-data instead. The everywhere initializer points
85
+ # Rails.public_path here via NATIVE_PUBLIC_DIR.
86
+ def extract_public_dir
87
+ return unless ENV["NATIVE_PACKAGED"] == "1"
88
+
89
+ source = File.join(root, "public")
90
+ return unless File.directory?(source)
91
+
92
+ target = File.join(app_data, "public")
93
+ FileUtils.rm_rf(target)
94
+ FileUtils.cp_r(source, target)
95
+ ENV["NATIVE_PUBLIC_DIR"] = target
96
+ end
97
+
98
+ def ensure_secret_key_base
99
+ secret_file = File.join(app_data, "secret_key_base")
100
+ unless File.exist?(secret_file)
101
+ File.write(secret_file, SecureRandom.hex(64))
102
+ File.chmod(0o600, secret_file)
103
+ end
104
+ ENV["SECRET_KEY_BASE"] ||= File.read(secret_file).strip
105
+ end
106
+
107
+ # If the desktop shell dies without cleanup (crash, force-quit, SIGKILL),
108
+ # shut the server down rather than lingering as an orphan. TERM to self
109
+ # gives Puma a graceful stop, which also stops Solid Queue's processes.
110
+ def watch_shell
111
+ shell_pid = ENV["NATIVE_SHELL_PID"].to_i
112
+ return unless shell_pid.positive?
113
+
114
+ Thread.new do
115
+ loop do
116
+ sleep 2
117
+ begin
118
+ Process.kill(0, shell_pid)
119
+ rescue Errno::ESRCH
120
+ Process.kill("TERM", Process.pid)
121
+ sleep 10
122
+ exit!(1)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def free_port
129
+ require "socket"
130
+ server = TCPServer.new("127.0.0.1", 0)
131
+ server.addr[1].to_s.tap { server.close }
132
+ end
133
+ end
134
+
135
+ def self.boot!(root:)
136
+ Boot.call(root: root)
137
+ end
138
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require_relative "../everywhere"
5
+ require_relative "framework"
6
+ require_relative "commands/build"
7
+ require_relative "commands/dev"
8
+ require_relative "commands/doctor"
9
+ require_relative "commands/install"
10
+ require_relative "commands/release"
11
+ require_relative "commands/platform/login"
12
+ require_relative "commands/platform/logout"
13
+ require_relative "commands/platform/auth_status"
14
+ require_relative "commands/platform/build"
15
+ require_relative "commands/platform/runner"
16
+
17
+ module Everywhere
18
+ module Commands
19
+ class Version < Dry::CLI::Command
20
+ desc "Print version"
21
+
22
+ def call(**)
23
+ puts "ruby_everywhere #{Everywhere::VERSION}"
24
+ end
25
+ end
26
+ end
27
+
28
+ module CLI
29
+ extend Dry::CLI::Registry
30
+
31
+ register "version", Commands::Version, aliases: ["-v", "--version"]
32
+ register "doctor", Commands::Doctor
33
+ register "install", Commands::Install
34
+ register "dev", Commands::Dev
35
+ register "build", Commands::Build
36
+ register "release", Commands::Release
37
+
38
+ register "platform login", Commands::PlatformLogin
39
+ register "platform logout", Commands::PlatformLogout
40
+ register "platform auth status", Commands::PlatformAuthStatus
41
+ register "platform build", Commands::PlatformBuild
42
+ register "platform runner", Commands::PlatformRunner
43
+ end
44
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "fileutils"
5
+ require "tmpdir"
6
+ require "shellwords"
7
+ require_relative "../shellout"
8
+ require_relative "../png"
9
+ require_relative "../paths"
10
+
11
+ module Everywhere
12
+ module Commands
13
+ # Press the app into a single-file binary and make it runnable on this Mac.
14
+ #
15
+ # Encodes the Xcode 26 / macOS 26 workarounds discovered in the Phase 0 spike:
16
+ # 1. CXX compiler shim (fmt consteval + folly __compressed_pair fixes)
17
+ # 2. BUNDLE_FORCE_RUBY_PLATFORM so native gems compile from source
18
+ # (tebako's strip corrupts precompiled gems' signatures -> SIGKILL)
19
+ # 3. Homebrew bison on PATH + LG_VADDR for tebako itself
20
+ # 4. Post-press ad-hoc re-sign (macOS 26 rejects the pressed signature)
21
+ class Build < Dry::CLI::Command
22
+ SHIM = File.expand_path("~/.tebako-shims/clang++")
23
+
24
+ desc "Package the app into a double-clickable desktop app (tebako press + shell + .app bundle)"
25
+
26
+ option :root, default: ".", desc: "App root directory"
27
+ option :output, default: nil, desc: "Output binary path (default: dist/<app>)"
28
+ option :ruby, default: "4.0.6", desc: "Ruby version to package"
29
+ option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
30
+ option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
31
+ option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
32
+ option :skip_press, type: :boolean, default: false, desc: "Reuse the existing pressed binary, only rebuild the .app"
33
+
34
+ def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb",
35
+ app_name: nil, shell_dir: nil, skip_press: false, **)
36
+ root_path = File.expand_path(root)
37
+ config = Everywhere::Config.load(root_path)
38
+ app_name ||= config.name
39
+
40
+ # Remote mode: thin shell around an already-deployed app. No Rails app
41
+ # required locally, nothing to press — just shell + config in a bundle.
42
+ if config.remote?
43
+ UI.step("#{UI.bold("remote")} mode → #{UI.cyan(config.remote_url)} #{UI.dim("(no local server packaged)")}")
44
+ app_dir = bundle_app(nil, app_name, shell_dir, config, dist_dir: File.expand_path("dist", root_path))
45
+ return
46
+ end
47
+
48
+ framework = Framework.detect(root)
49
+ output ||= File.join("dist", File.basename(framework.root))
50
+ output_path = File.expand_path(output)
51
+ entry_path = File.join(framework.root, entry)
52
+
53
+ unless File.exist?(entry_path)
54
+ UI.die!("#{entry} not found in #{framework.root} — the app needs a packaged-boot entry point")
55
+ end
56
+
57
+ UI.step("#{UI.bold(framework.name)} app at #{framework.root}")
58
+
59
+ if skip_press
60
+ UI.die!("--skip-press given but #{output} doesn't exist") unless File.exist?(output_path)
61
+ UI.step("reusing pressed binary #{output}")
62
+ else
63
+ press!(framework, entry, ruby, output_path)
64
+ end
65
+
66
+ bundle_app(output_path, app_name, shell_dir, config) if RUBY_PLATFORM.include?("darwin")
67
+
68
+ UI.success("built #{output} (#{(File.size(output_path) / 1024.0 / 1024.0).round}MB)")
69
+ end
70
+
71
+ private
72
+
73
+ def press!(framework, entry, ruby, output_path)
74
+ UI.step("precompiling assets")
75
+ Shellout.run!(framework.precompile_env, framework.precompile_command, chdir: framework.root)
76
+
77
+ out_dir = File.dirname(output_path)
78
+ # Tebako packages the whole app root, so stale build products in an
79
+ # in-root dist/ (previous binaries, .app bundles, logs) would get baked
80
+ # into the new binary. dist/ is ours — clear it.
81
+ FileUtils.rm_rf(out_dir) if out_dir.start_with?(framework.root + File::SEPARATOR)
82
+ FileUtils.mkdir_p(out_dir)
83
+ # Press into a staging dir OUTSIDE the app root for the same reason
84
+ # (tebako prints a big warning about exactly this).
85
+ staging = File.join(Dir.mktmpdir("everywhere-press"), File.basename(output_path))
86
+ log = "#{output_path}.build.log"
87
+
88
+ UI.step("rbe-tebako press (Ruby #{ruby}) #{UI.dim("— grab a coffee on first run")}")
89
+ UI.step(UI.dim("full build log: #{log}"))
90
+ Shellout.run_logged!(press_env, ["rbe-tebako", "press",
91
+ "--root=#{framework.root}", "--entry-point=#{entry}",
92
+ "--Ruby=#{ruby}", "--output=#{staging}"], log: log)
93
+
94
+ # Tebako's final strip step only warns (and still exits 0) if it can't
95
+ # write the output, so verify the artifact instead of trusting exit codes.
96
+ UI.die!("tebako reported success but no binary was created") unless File.exist?(staging)
97
+ FileUtils.mv(staging, output_path)
98
+
99
+ UI.step("re-signing for macOS")
100
+ Shellout.run!({}, "codesign", "--remove-signature", output_path)
101
+ Shellout.run!({}, "codesign", "-s", "-", output_path)
102
+
103
+ # Leaving precompiled assets behind shadows dev-mode assets (new JS
104
+ # wouldn't load in `every dev` until someone clobbers by hand).
105
+ UI.step("cleaning up precompiled assets")
106
+ Shellout.run!(framework.precompile_env, framework.cleanup_command, chdir: framework.root)
107
+
108
+ surface_warnings(log)
109
+ end
110
+
111
+ # Assemble dist/<App Name>.app: the release shell binary + the pressed
112
+ # Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
113
+ # just a directory layout, so we build it by hand — no tauri-cli needed.
114
+ def bundle_app(server_binary, app_name, shell_dir, config, dist_dir: nil)
115
+ shell_dir ||= Everywhere::Paths.shell_dir!
116
+ dist_dir ||= File.dirname(server_binary)
117
+
118
+ icon_source = config.icon
119
+ if icon_source && !File.exist?(icon_source)
120
+ UI.warn "app icon #{icon_source} not found — using placeholder"
121
+ icon_source = nil
122
+ end
123
+ UI.step("app icon: #{icon_source ? icon_source.sub("#{config.root}/", "") : "placeholder"}")
124
+
125
+ # Tauri bakes icons/icon.png into the shell binary and sets it as the
126
+ # Dock icon at runtime (stomping the bundle's icns after launch), so the
127
+ # shell must be COMPILED with the app's icon. Swap it in for the build,
128
+ # restore the placeholder after; touch build.rs so tauri-build re-embeds.
129
+ shell_icon = File.join(shell_dir, "icons", "icon.png")
130
+ placeholder_backup = "#{shell_icon}.placeholder"
131
+ if icon_source
132
+ FileUtils.cp(shell_icon, placeholder_backup) unless File.exist?(placeholder_backup)
133
+ unless Everywhere::PNG.ensure_rgba(icon_source, shell_icon)
134
+ UI.warn "couldn't convert #{File.basename(icon_source)} to RGBA (unusual PNG flavor) — Dock icon will use the placeholder after launch"
135
+ FileUtils.cp(placeholder_backup, shell_icon)
136
+ end
137
+ FileUtils.touch(File.join(shell_dir, "build.rs"))
138
+ end
139
+
140
+ UI.step("building shell #{UI.dim("(cargo release)")}")
141
+ begin
142
+ Shellout.run!({}, "cargo", "build", "--release", chdir: shell_dir)
143
+ ensure
144
+ if icon_source && File.exist?(placeholder_backup)
145
+ FileUtils.mv(placeholder_backup, shell_icon)
146
+ FileUtils.touch(File.join(shell_dir, "build.rs"))
147
+ end
148
+ end
149
+ shell_bin = Dir[File.join(shell_dir, "target/release/*")]
150
+ .find { |f| File.file?(f) && File.executable?(f) && !f.end_with?(".d") }
151
+ UI.die!("couldn't find release shell binary in #{shell_dir}/target/release") unless shell_bin
152
+
153
+ app_dir = File.join(dist_dir, "#{app_name}.app")
154
+ macos = File.join(app_dir, "Contents", "MacOS")
155
+ resources = File.join(app_dir, "Contents", "Resources")
156
+ UI.step("assembling #{File.basename(app_dir)}")
157
+ FileUtils.rm_rf(app_dir)
158
+ FileUtils.mkdir_p([macos, resources])
159
+
160
+ exe_name = app_name.gsub(/\s+/, "")
161
+ FileUtils.cp(shell_bin, File.join(macos, exe_name))
162
+ FileUtils.cp(server_binary, File.join(macos, "app-server")) if server_binary
163
+
164
+ File.write(File.join(resources, "everywhere.json"), config.to_shell_json)
165
+ if (splash = config.splash)
166
+ if File.exist?(splash)
167
+ FileUtils.cp(splash, File.join(resources, "splash.html"))
168
+ UI.step("custom splash: #{splash.sub("#{config.root}/", "")}")
169
+ else
170
+ UI.warn "splash #{splash} not found — using the default splash"
171
+ end
172
+ end
173
+ icns = make_icns(icon_source || File.join(shell_dir, "icons", "icon.png"), resources)
174
+ File.write(File.join(app_dir, "Contents", "Info.plist"),
175
+ info_plist(app_name, exe_name, icns, config.bundle_id, config.version))
176
+
177
+ UI.step("signing #{File.basename(app_dir)}")
178
+ Shellout.run!({}, "codesign", "-s", "-", "-f", File.join(macos, "app-server")) if server_binary
179
+ Shellout.run!({}, "codesign", "-s", "-", "-f", File.join(macos, exe_name))
180
+ Shellout.run!({}, "codesign", "-s", "-", "-f", app_dir)
181
+
182
+ # macOS caches app icons by bundle path; re-register so icon changes
183
+ # show up without the user hunting down cache-flush incantations.
184
+ Shellout.run?("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f #{app_dir.shellescape} 2>/dev/null")
185
+
186
+ UI.success("app bundle: #{app_dir.sub("#{Dir.pwd}/", "")} #{UI.dim("(open it!)")}")
187
+ end
188
+
189
+ # macOS ships everything needed to make an .icns from a png.
190
+ def make_icns(png, resources_dir)
191
+ return nil unless File.exist?(png)
192
+
193
+ require "shellwords"
194
+ Dir.mktmpdir do |tmp|
195
+ iconset = File.join(tmp, "App.iconset")
196
+ FileUtils.mkdir_p(iconset)
197
+ [16, 32, 128, 256, 512].each do |size|
198
+ [[size, "icon_#{size}x#{size}.png"], [size * 2, "icon_#{size}x#{size}@2x.png"]].each do |px, name|
199
+ Shellout.run?("sips -z #{px} #{px} #{png.shellescape} --out #{File.join(iconset, name).shellescape} >/dev/null 2>&1")
200
+ end
201
+ end
202
+ out = File.join(resources_dir, "AppIcon.icns")
203
+ return "AppIcon" if Shellout.run?("iconutil -c icns #{iconset.shellescape} -o #{out.shellescape} >/dev/null 2>&1")
204
+ end
205
+ nil
206
+ end
207
+
208
+ # CFBundleShortVersionString is the user-facing marketing version; both it
209
+ # and CFBundleVersion come from `app.version` in everywhere.yml (via
210
+ # config.version), so the bundle and the release.json receipt agree.
211
+ def info_plist(app_name, exe_name, icon, identifier, version)
212
+ icon_keys = icon ? " <key>CFBundleIconFile</key><string>#{icon}</string>\n" : ""
213
+ <<~PLIST
214
+ <?xml version="1.0" encoding="UTF-8"?>
215
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
216
+ <plist version="1.0">
217
+ <dict>
218
+ <key>CFBundleName</key><string>#{app_name}</string>
219
+ <key>CFBundleDisplayName</key><string>#{app_name}</string>
220
+ <key>CFBundleExecutable</key><string>#{exe_name}</string>
221
+ <key>CFBundleIdentifier</key><string>#{identifier}</string>
222
+ <key>CFBundlePackageType</key><string>APPL</string>
223
+ <key>CFBundleShortVersionString</key><string>#{version}</string>
224
+ <key>CFBundleVersion</key><string>#{version}</string>
225
+ <key>LSMinimumSystemVersion</key><string>13.0</string>
226
+ <key>NSHighResolutionCapable</key><true/>
227
+ #{icon_keys}</dict>
228
+ </plist>
229
+ PLIST
230
+ end
231
+
232
+ private
233
+
234
+ # Tebako's output is thousands of lines; re-surface the warnings that
235
+ # actually matter to the app developer after the build finishes.
236
+ def surface_warnings(log)
237
+ return unless File.exist?(log)
238
+
239
+ text = File.read(log)
240
+
241
+ if text.match?(/could not strip|string table not at the end/)
242
+ UI.warn "some binaries couldn't be stripped of debug symbols (harmless; " \
243
+ "binary is a bit larger than it could be)"
244
+ end
245
+ if text.match?(/incbin is incompatible with bitcode/)
246
+ # Bitcode was removed by Apple in Xcode 14 — irrelevant, don't alarm anyone.
247
+ nil
248
+ end
249
+
250
+ press_warnings = text.scan(/^.*(?:WARNING|Warning):(?!.*bitcode).*$/).size
251
+ UI.warn "tebako emitted #{press_warnings} warning(s) — details in the build log" if press_warnings.positive?
252
+ end
253
+
254
+ def press_env
255
+ env = {
256
+ "BUNDLE_FORCE_RUBY_PLATFORM" => "true",
257
+ "LG_VADDR" => "39"
258
+ }
259
+ env["CXX"] = SHIM if File.exist?(SHIM)
260
+ bison = `brew --prefix bison 2>/dev/null`.strip
261
+ env["PATH"] = "#{bison}/bin:#{ENV.fetch("PATH")}" unless bison.empty?
262
+ env
263
+ end
264
+
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "socket"
5
+ require_relative "../shellout"
6
+ require_relative "../paths"
7
+
8
+ module Everywhere
9
+ module Commands
10
+ # Run the app's normal dev server and open the desktop shell pointed at it.
11
+ # No packaging: the shell skips the sidecar when NATIVE_DEV_URL is set,
12
+ # so the edit-refresh loop is untouched Rails/Hanami.
13
+ class Dev < Dry::CLI::Command
14
+ desc "Run the dev server inside the desktop shell (live reload, no packaging)"
15
+
16
+ option :port, default: "3000", desc: "Dev server port"
17
+ option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
18
+
19
+ def call(port: "3000", shell_dir: nil, **)
20
+ framework = Framework.detect
21
+ config = Everywhere::Config.load(framework.root)
22
+ shell_dir ||= Everywhere::Paths.shell_dir!
23
+
24
+ server_pid = nil
25
+ if port_open?(port)
26
+ UI.warn "something is ALREADY RUNNING on port #{port} — attaching to it instead of starting"
27
+ UI.warn "#{UI.dim("your Procfile.dev/watchers are NOT running; stop that server or use --port if this isn't yours")}"
28
+ else
29
+ UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
30
+ server_pid = Shellout.spawn({ "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
31
+ wait_for_port(port)
32
+ end
33
+
34
+ dev_url = "http://127.0.0.1:#{port}#{config.entry_path}"
35
+ UI.step("opening desktop shell → #{UI.cyan(dev_url)}")
36
+ Shellout.run!({ "NATIVE_DEV_URL" => dev_url, "NATIVE_CONFIG" => config.to_shell_json },
37
+ "cargo", "run", chdir: shell_dir)
38
+ ensure
39
+ if server_pid
40
+ Process.kill("TERM", -Process.getpgid(server_pid)) rescue Process.kill("TERM", server_pid) rescue nil
41
+ Process.wait(server_pid) rescue nil
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def port_open?(port)
48
+ TCPSocket.new("127.0.0.1", Integer(port)).close
49
+ true
50
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
51
+ false
52
+ end
53
+
54
+ def wait_for_port(port, timeout: 60)
55
+ deadline = Time.now + timeout
56
+ sleep 0.5 until port_open?(port) || Time.now > deadline
57
+ UI.die!("dev server never came up on port #{port}") unless port_open?(port)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require_relative "../shellout"
5
+
6
+ module Everywhere
7
+ module Commands
8
+ # Check that this machine can build desktop apps.
9
+ class Doctor < Dry::CLI::Command
10
+ desc "Check the toolchain (tebako, rust, brew deps, Xcode 26 shims)"
11
+
12
+ def call(**)
13
+ ok = true
14
+ ok &= check("rbe-tebako installed") { Shellout.run?("rbe-tebako help > /dev/null 2>&1") }
15
+ ok &= check("cargo installed") { Shellout.run?("cargo --version > /dev/null 2>&1") }
16
+ ok &= check("homebrew bison") { !`brew --prefix bison 2>/dev/null`.strip.empty? }
17
+
18
+ if RUBY_PLATFORM.include?("darwin") && clang_major >= 21
19
+ ok &= check("Xcode #{clang_major} CXX shim (~/.tebako-shims/clang++)") do
20
+ File.executable?(File.expand_path("~/.tebako-shims/clang++"))
21
+ end
22
+ ok &= check("tebako force_ruby_platform patch") do
23
+ helper = `gem contents rbe-tebako 2>/dev/null`.lines.map(&:strip).find { |l| l.end_with?("deploy_helper.rb") }
24
+ helper.nil? || File.read(helper).include?(%(@force_ruby_platform = "true"
25
+ @nokogiri_option = "--no-use-system-libraries"))
26
+ end
27
+ ok &= check("tebako rbconfig warnflags patch") do
28
+ stash = Dir[File.expand_path("~/.tebako/deps/stash_*/lib/ruby/*/**/rbconfig.rb")]
29
+ stash.empty? || stash.all? { |f| File.read(f).include?("default-const-init") }
30
+ end
31
+ end
32
+
33
+ puts
34
+ ok ? UI.success("ready to build") : UI.bad("fix the items above (see project docs / memory for recipes)")
35
+ exit(1) unless ok
36
+ end
37
+
38
+ private
39
+
40
+ def check(label)
41
+ passed = yield
42
+ passed ? UI.ok(label) : UI.bad(label)
43
+ passed
44
+ rescue StandardError
45
+ UI.bad(label)
46
+ false
47
+ end
48
+
49
+ def clang_major
50
+ @clang_major ||= `clang --version 2>/dev/null`[/version (\d+)/, 1].to_i
51
+ end
52
+ end
53
+ end
54
+ end