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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75b09aedaa84953cf39a5b6bc7e793f660998f297d5c7cc74ed453fef68172f3
|
|
4
|
+
data.tar.gz: f1970350eaaedaf2fed2fd6123a25c152f196019256d5c2ff848a0a262f589c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f611fb7632f07a4931b659166ad6c1e0e773b7fa979b38c66f109cb0c22ace4c9710bd977e112d40bceee6efdd358fe5283262d7218f60e36f267e4456e6f5f
|
|
7
|
+
data.tar.gz: '08def4faa79e14355864dd66369a8cfb06884b062186234d96478c9cbe3edd9957f14e46319bd2276f5927728ccea0ce442ae6d216b2bc851cd059de815f84a3'
|
data/exe/every
ADDED
data/exe/rbe
ADDED
|
@@ -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,272 @@
|
|
|
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
|
+
|
|
10
|
+
module Everywhere
|
|
11
|
+
module Commands
|
|
12
|
+
# Press the app into a single-file binary and make it runnable on this Mac.
|
|
13
|
+
#
|
|
14
|
+
# Encodes the Xcode 26 / macOS 26 workarounds discovered in the Phase 0 spike:
|
|
15
|
+
# 1. CXX compiler shim (fmt consteval + folly __compressed_pair fixes)
|
|
16
|
+
# 2. BUNDLE_FORCE_RUBY_PLATFORM so native gems compile from source
|
|
17
|
+
# (tebako's strip corrupts precompiled gems' signatures -> SIGKILL)
|
|
18
|
+
# 3. Homebrew bison on PATH + LG_VADDR for tebako itself
|
|
19
|
+
# 4. Post-press ad-hoc re-sign (macOS 26 rejects the pressed signature)
|
|
20
|
+
class Build < Dry::CLI::Command
|
|
21
|
+
SHIM = File.expand_path("~/.tebako-shims/clang++")
|
|
22
|
+
|
|
23
|
+
desc "Package the app into a double-clickable desktop app (tebako press + shell + .app bundle)"
|
|
24
|
+
|
|
25
|
+
option :root, default: ".", desc: "App root directory"
|
|
26
|
+
option :output, default: nil, desc: "Output binary path (default: dist/<app>)"
|
|
27
|
+
option :ruby, default: "4.0.6", desc: "Ruby version to package"
|
|
28
|
+
option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
|
|
29
|
+
option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
|
|
30
|
+
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
31
|
+
option :skip_press, type: :boolean, default: false, desc: "Reuse the existing pressed binary, only rebuild the .app"
|
|
32
|
+
|
|
33
|
+
def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb",
|
|
34
|
+
app_name: nil, shell_dir: nil, skip_press: false, **)
|
|
35
|
+
root_path = File.expand_path(root)
|
|
36
|
+
config = Everywhere::Config.load(root_path)
|
|
37
|
+
app_name ||= config.name
|
|
38
|
+
|
|
39
|
+
# Remote mode: thin shell around an already-deployed app. No Rails app
|
|
40
|
+
# required locally, nothing to press — just shell + config in a bundle.
|
|
41
|
+
if config.remote?
|
|
42
|
+
UI.step("#{UI.bold("remote")} mode → #{UI.cyan(config.remote_url)} #{UI.dim("(no local server packaged)")}")
|
|
43
|
+
app_dir = bundle_app(nil, app_name, shell_dir, config, dist_dir: File.expand_path("dist", root_path))
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
framework = Framework.detect(root)
|
|
48
|
+
output ||= File.join("dist", File.basename(framework.root))
|
|
49
|
+
output_path = File.expand_path(output)
|
|
50
|
+
entry_path = File.join(framework.root, entry)
|
|
51
|
+
|
|
52
|
+
unless File.exist?(entry_path)
|
|
53
|
+
UI.die!("#{entry} not found in #{framework.root} — the app needs a packaged-boot entry point")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
UI.step("#{UI.bold(framework.name)} app at #{framework.root}")
|
|
57
|
+
|
|
58
|
+
if skip_press
|
|
59
|
+
UI.die!("--skip-press given but #{output} doesn't exist") unless File.exist?(output_path)
|
|
60
|
+
UI.step("reusing pressed binary #{output}")
|
|
61
|
+
else
|
|
62
|
+
press!(framework, entry, ruby, output_path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
bundle_app(output_path, app_name, shell_dir, config) if RUBY_PLATFORM.include?("darwin")
|
|
66
|
+
|
|
67
|
+
UI.success("built #{output} (#{(File.size(output_path) / 1024.0 / 1024.0).round}MB)")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def press!(framework, entry, ruby, output_path)
|
|
73
|
+
UI.step("precompiling assets")
|
|
74
|
+
Shellout.run!(framework.precompile_env, framework.precompile_command, chdir: framework.root)
|
|
75
|
+
|
|
76
|
+
out_dir = File.dirname(output_path)
|
|
77
|
+
# Tebako packages the whole app root, so stale build products in an
|
|
78
|
+
# in-root dist/ (previous binaries, .app bundles, logs) would get baked
|
|
79
|
+
# into the new binary. dist/ is ours — clear it.
|
|
80
|
+
FileUtils.rm_rf(out_dir) if out_dir.start_with?(framework.root + File::SEPARATOR)
|
|
81
|
+
FileUtils.mkdir_p(out_dir)
|
|
82
|
+
# Press into a staging dir OUTSIDE the app root for the same reason
|
|
83
|
+
# (tebako prints a big warning about exactly this).
|
|
84
|
+
staging = File.join(Dir.mktmpdir("everywhere-press"), File.basename(output_path))
|
|
85
|
+
log = "#{output_path}.build.log"
|
|
86
|
+
|
|
87
|
+
UI.step("rbe-tebako press (Ruby #{ruby}) #{UI.dim("— grab a coffee on first run")}")
|
|
88
|
+
UI.step(UI.dim("full build log: #{log}"))
|
|
89
|
+
Shellout.run_logged!(press_env, ["rbe-tebako", "press",
|
|
90
|
+
"--root=#{framework.root}", "--entry-point=#{entry}",
|
|
91
|
+
"--Ruby=#{ruby}", "--output=#{staging}"], log: log)
|
|
92
|
+
|
|
93
|
+
# Tebako's final strip step only warns (and still exits 0) if it can't
|
|
94
|
+
# write the output, so verify the artifact instead of trusting exit codes.
|
|
95
|
+
UI.die!("tebako reported success but no binary was created") unless File.exist?(staging)
|
|
96
|
+
FileUtils.mv(staging, output_path)
|
|
97
|
+
|
|
98
|
+
UI.step("re-signing for macOS")
|
|
99
|
+
Shellout.run!({}, "codesign", "--remove-signature", output_path)
|
|
100
|
+
Shellout.run!({}, "codesign", "-s", "-", output_path)
|
|
101
|
+
|
|
102
|
+
# Leaving precompiled assets behind shadows dev-mode assets (new JS
|
|
103
|
+
# wouldn't load in `every dev` until someone clobbers by hand).
|
|
104
|
+
UI.step("cleaning up precompiled assets")
|
|
105
|
+
Shellout.run!(framework.precompile_env, framework.cleanup_command, chdir: framework.root)
|
|
106
|
+
|
|
107
|
+
surface_warnings(log)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Assemble dist/<App Name>.app: the release shell binary + the pressed
|
|
111
|
+
# Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
|
|
112
|
+
# just a directory layout, so we build it by hand — no tauri-cli needed.
|
|
113
|
+
def bundle_app(server_binary, app_name, shell_dir, config, dist_dir: nil)
|
|
114
|
+
shell_dir ||= find_shell_dir
|
|
115
|
+
dist_dir ||= File.dirname(server_binary)
|
|
116
|
+
|
|
117
|
+
icon_source = config.icon
|
|
118
|
+
if icon_source && !File.exist?(icon_source)
|
|
119
|
+
UI.warn "app icon #{icon_source} not found — using placeholder"
|
|
120
|
+
icon_source = nil
|
|
121
|
+
end
|
|
122
|
+
UI.step("app icon: #{icon_source ? icon_source.sub("#{config.root}/", "") : "placeholder"}")
|
|
123
|
+
|
|
124
|
+
# Tauri bakes icons/icon.png into the shell binary and sets it as the
|
|
125
|
+
# Dock icon at runtime (stomping the bundle's icns after launch), so the
|
|
126
|
+
# shell must be COMPILED with the app's icon. Swap it in for the build,
|
|
127
|
+
# restore the placeholder after; touch build.rs so tauri-build re-embeds.
|
|
128
|
+
shell_icon = File.join(shell_dir, "icons", "icon.png")
|
|
129
|
+
placeholder_backup = "#{shell_icon}.placeholder"
|
|
130
|
+
if icon_source
|
|
131
|
+
FileUtils.cp(shell_icon, placeholder_backup) unless File.exist?(placeholder_backup)
|
|
132
|
+
unless Everywhere::PNG.ensure_rgba(icon_source, shell_icon)
|
|
133
|
+
UI.warn "couldn't convert #{File.basename(icon_source)} to RGBA (unusual PNG flavor) — Dock icon will use the placeholder after launch"
|
|
134
|
+
FileUtils.cp(placeholder_backup, shell_icon)
|
|
135
|
+
end
|
|
136
|
+
FileUtils.touch(File.join(shell_dir, "build.rs"))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
UI.step("building shell #{UI.dim("(cargo release)")}")
|
|
140
|
+
begin
|
|
141
|
+
Shellout.run!({}, "cargo", "build", "--release", chdir: shell_dir)
|
|
142
|
+
ensure
|
|
143
|
+
if icon_source && File.exist?(placeholder_backup)
|
|
144
|
+
FileUtils.mv(placeholder_backup, shell_icon)
|
|
145
|
+
FileUtils.touch(File.join(shell_dir, "build.rs"))
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
shell_bin = Dir[File.join(shell_dir, "target/release/*")]
|
|
149
|
+
.find { |f| File.file?(f) && File.executable?(f) && !f.end_with?(".d") }
|
|
150
|
+
UI.die!("couldn't find release shell binary in #{shell_dir}/target/release") unless shell_bin
|
|
151
|
+
|
|
152
|
+
app_dir = File.join(dist_dir, "#{app_name}.app")
|
|
153
|
+
macos = File.join(app_dir, "Contents", "MacOS")
|
|
154
|
+
resources = File.join(app_dir, "Contents", "Resources")
|
|
155
|
+
UI.step("assembling #{File.basename(app_dir)}")
|
|
156
|
+
FileUtils.rm_rf(app_dir)
|
|
157
|
+
FileUtils.mkdir_p([macos, resources])
|
|
158
|
+
|
|
159
|
+
exe_name = app_name.gsub(/\s+/, "")
|
|
160
|
+
FileUtils.cp(shell_bin, File.join(macos, exe_name))
|
|
161
|
+
FileUtils.cp(server_binary, File.join(macos, "app-server")) if server_binary
|
|
162
|
+
|
|
163
|
+
File.write(File.join(resources, "everywhere.json"), config.to_shell_json)
|
|
164
|
+
if (splash = config.splash)
|
|
165
|
+
if File.exist?(splash)
|
|
166
|
+
FileUtils.cp(splash, File.join(resources, "splash.html"))
|
|
167
|
+
UI.step("custom splash: #{splash.sub("#{config.root}/", "")}")
|
|
168
|
+
else
|
|
169
|
+
UI.warn "splash #{splash} not found — using the default splash"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
icns = make_icns(icon_source || File.join(shell_dir, "icons", "icon.png"), resources)
|
|
173
|
+
File.write(File.join(app_dir, "Contents", "Info.plist"),
|
|
174
|
+
info_plist(app_name, exe_name, icns, config.bundle_id, config.version))
|
|
175
|
+
|
|
176
|
+
UI.step("signing #{File.basename(app_dir)}")
|
|
177
|
+
Shellout.run!({}, "codesign", "-s", "-", "-f", File.join(macos, "app-server")) if server_binary
|
|
178
|
+
Shellout.run!({}, "codesign", "-s", "-", "-f", File.join(macos, exe_name))
|
|
179
|
+
Shellout.run!({}, "codesign", "-s", "-", "-f", app_dir)
|
|
180
|
+
|
|
181
|
+
# macOS caches app icons by bundle path; re-register so icon changes
|
|
182
|
+
# show up without the user hunting down cache-flush incantations.
|
|
183
|
+
Shellout.run?("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f #{app_dir.shellescape} 2>/dev/null")
|
|
184
|
+
|
|
185
|
+
UI.success("app bundle: #{app_dir.sub("#{Dir.pwd}/", "")} #{UI.dim("(open it!)")}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def find_shell_dir
|
|
189
|
+
candidates = ["shell/src-tauri", "../shell/src-tauri", "src-tauri"]
|
|
190
|
+
found = candidates.find { |c| File.exist?(File.join(c, "tauri.conf.json")) }
|
|
191
|
+
found ? File.expand_path(found) : UI.die!("couldn't find the shell (looked in #{candidates.join(", ")}); pass --shell-dir")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# macOS ships everything needed to make an .icns from a png.
|
|
195
|
+
def make_icns(png, resources_dir)
|
|
196
|
+
return nil unless File.exist?(png)
|
|
197
|
+
|
|
198
|
+
require "shellwords"
|
|
199
|
+
Dir.mktmpdir do |tmp|
|
|
200
|
+
iconset = File.join(tmp, "App.iconset")
|
|
201
|
+
FileUtils.mkdir_p(iconset)
|
|
202
|
+
[16, 32, 128, 256, 512].each do |size|
|
|
203
|
+
[[size, "icon_#{size}x#{size}.png"], [size * 2, "icon_#{size}x#{size}@2x.png"]].each do |px, name|
|
|
204
|
+
Shellout.run?("sips -z #{px} #{px} #{png.shellescape} --out #{File.join(iconset, name).shellescape} >/dev/null 2>&1")
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
out = File.join(resources_dir, "AppIcon.icns")
|
|
208
|
+
return "AppIcon" if Shellout.run?("iconutil -c icns #{iconset.shellescape} -o #{out.shellescape} >/dev/null 2>&1")
|
|
209
|
+
end
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# CFBundleShortVersionString is the user-facing marketing version; both it
|
|
214
|
+
# and CFBundleVersion come from `app.version` in everywhere.yml (via
|
|
215
|
+
# config.version), so the bundle and the release.json receipt agree.
|
|
216
|
+
def info_plist(app_name, exe_name, icon, identifier, version)
|
|
217
|
+
icon_keys = icon ? " <key>CFBundleIconFile</key><string>#{icon}</string>\n" : ""
|
|
218
|
+
<<~PLIST
|
|
219
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
220
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
221
|
+
<plist version="1.0">
|
|
222
|
+
<dict>
|
|
223
|
+
<key>CFBundleName</key><string>#{app_name}</string>
|
|
224
|
+
<key>CFBundleDisplayName</key><string>#{app_name}</string>
|
|
225
|
+
<key>CFBundleExecutable</key><string>#{exe_name}</string>
|
|
226
|
+
<key>CFBundleIdentifier</key><string>#{identifier}</string>
|
|
227
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
228
|
+
<key>CFBundleShortVersionString</key><string>#{version}</string>
|
|
229
|
+
<key>CFBundleVersion</key><string>#{version}</string>
|
|
230
|
+
<key>LSMinimumSystemVersion</key><string>13.0</string>
|
|
231
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
232
|
+
#{icon_keys}</dict>
|
|
233
|
+
</plist>
|
|
234
|
+
PLIST
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
# Tebako's output is thousands of lines; re-surface the warnings that
|
|
240
|
+
# actually matter to the app developer after the build finishes.
|
|
241
|
+
def surface_warnings(log)
|
|
242
|
+
return unless File.exist?(log)
|
|
243
|
+
|
|
244
|
+
text = File.read(log)
|
|
245
|
+
|
|
246
|
+
if text.match?(/could not strip|string table not at the end/)
|
|
247
|
+
UI.warn "some binaries couldn't be stripped of debug symbols (harmless; " \
|
|
248
|
+
"binary is a bit larger than it could be)"
|
|
249
|
+
end
|
|
250
|
+
if text.match?(/incbin is incompatible with bitcode/)
|
|
251
|
+
# Bitcode was removed by Apple in Xcode 14 — irrelevant, don't alarm anyone.
|
|
252
|
+
nil
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
press_warnings = text.scan(/^.*(?:WARNING|Warning):(?!.*bitcode).*$/).size
|
|
256
|
+
UI.warn "tebako emitted #{press_warnings} warning(s) — details in the build log" if press_warnings.positive?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def press_env
|
|
260
|
+
env = {
|
|
261
|
+
"BUNDLE_FORCE_RUBY_PLATFORM" => "true",
|
|
262
|
+
"LG_VADDR" => "39"
|
|
263
|
+
}
|
|
264
|
+
env["CXX"] = SHIM if File.exist?(SHIM)
|
|
265
|
+
bison = `brew --prefix bison 2>/dev/null`.strip
|
|
266
|
+
env["PATH"] = "#{bison}/bin:#{ENV.fetch("PATH")}" unless bison.empty?
|
|
267
|
+
env
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require "socket"
|
|
5
|
+
require_relative "../shellout"
|
|
6
|
+
|
|
7
|
+
module Everywhere
|
|
8
|
+
module Commands
|
|
9
|
+
# Run the app's normal dev server and open the desktop shell pointed at it.
|
|
10
|
+
# No packaging: the shell skips the sidecar when NATIVE_DEV_URL is set,
|
|
11
|
+
# so the edit-refresh loop is untouched Rails/Hanami.
|
|
12
|
+
class Dev < Dry::CLI::Command
|
|
13
|
+
desc "Run the dev server inside the desktop shell (live reload, no packaging)"
|
|
14
|
+
|
|
15
|
+
option :port, default: "3000", desc: "Dev server port"
|
|
16
|
+
option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
|
|
17
|
+
|
|
18
|
+
def call(port: "3000", shell_dir: nil, **)
|
|
19
|
+
framework = Framework.detect
|
|
20
|
+
config = Everywhere::Config.load(framework.root)
|
|
21
|
+
shell_dir ||= find_shell_dir
|
|
22
|
+
|
|
23
|
+
server_pid = nil
|
|
24
|
+
if port_open?(port)
|
|
25
|
+
UI.warn "something is ALREADY RUNNING on port #{port} — attaching to it instead of starting"
|
|
26
|
+
UI.warn "#{UI.dim("your Procfile.dev/watchers are NOT running; stop that server or use --port if this isn't yours")}"
|
|
27
|
+
else
|
|
28
|
+
UI.step("starting #{UI.bold(framework.name)} dev server #{UI.dim("(" + framework.dev_command + ")")}")
|
|
29
|
+
server_pid = Shellout.spawn({ "PORT" => port.to_s }, framework.dev_command, chdir: framework.root)
|
|
30
|
+
wait_for_port(port)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
dev_url = "http://127.0.0.1:#{port}#{config.entry_path}"
|
|
34
|
+
UI.step("opening desktop shell → #{UI.cyan(dev_url)}")
|
|
35
|
+
Shellout.run!({ "NATIVE_DEV_URL" => dev_url, "NATIVE_CONFIG" => config.to_shell_json },
|
|
36
|
+
"cargo", "run", chdir: shell_dir)
|
|
37
|
+
ensure
|
|
38
|
+
if server_pid
|
|
39
|
+
Process.kill("TERM", -Process.getpgid(server_pid)) rescue Process.kill("TERM", server_pid) rescue nil
|
|
40
|
+
Process.wait(server_pid) rescue nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def find_shell_dir
|
|
47
|
+
candidates = ["shell/src-tauri", "../shell/src-tauri", "src-tauri"]
|
|
48
|
+
found = candidates.find { |c| File.exist?(File.join(c, "tauri.conf.json")) }
|
|
49
|
+
found or UI.die!("couldn't find the shell (looked in #{candidates.join(", ")}); pass --shell-dir")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def port_open?(port)
|
|
53
|
+
TCPSocket.new("127.0.0.1", Integer(port)).close
|
|
54
|
+
true
|
|
55
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def wait_for_port(port, timeout: 60)
|
|
60
|
+
deadline = Time.now + timeout
|
|
61
|
+
sleep 0.5 until port_open?(port) || Time.now > deadline
|
|
62
|
+
UI.die!("dev server never came up on port #{port}") unless port_open?(port)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
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
|