rubox 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 +7 -0
- data/LICENSE +21 -0
- data/data/Dockerfile.ruby-build +119 -0
- data/data/ext/stub.c +489 -0
- data/data/ext/write-footer.c +38 -0
- data/data/prune-list.conf +40 -0
- data/data/scripts/_common.sh +17 -0
- data/data/scripts/build-ruby.sh +214 -0
- data/data/scripts/fix-dylibs.sh +93 -0
- data/data/scripts/package.sh +544 -0
- data/data/stubs/stub-aarch64-darwin +0 -0
- data/data/stubs/stub-aarch64-linux +0 -0
- data/data/stubs/stub-x86_64-linux +0 -0
- data/data/stubs/write-footer-aarch64-darwin +0 -0
- data/data/stubs/write-footer-aarch64-linux +0 -0
- data/data/stubs/write-footer-x86_64-linux +0 -0
- data/exe/rubox +3 -0
- data/lib/rubox/builder.rb +130 -0
- data/lib/rubox/cli.rb +214 -0
- data/lib/rubox/detector.rb +105 -0
- data/lib/rubox/packager.rb +118 -0
- data/lib/rubox/platform.rb +40 -0
- data/lib/rubox/version.rb +3 -0
- data/lib/rubox.rb +31 -0
- metadata +68 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Rubox
|
|
7
|
+
class Builder
|
|
8
|
+
# Pre-built Ruby binaries are hosted at this base URL.
|
|
9
|
+
# Falls back to building from source if download fails.
|
|
10
|
+
DOWNLOAD_BASE = "https://github.com/khasinski/rubox/releases/download"
|
|
11
|
+
|
|
12
|
+
attr_reader :ruby_version, :target, :output_dir
|
|
13
|
+
|
|
14
|
+
def initialize(ruby_version:, target:, output_dir: nil)
|
|
15
|
+
@ruby_version = ruby_version
|
|
16
|
+
@target = target
|
|
17
|
+
@output_dir = output_dir || "build/ruby-#{ruby_version}-#{target}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def built?
|
|
21
|
+
File.exist?(File.join(output_dir, "bin", "ruby"))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build!
|
|
25
|
+
return if built?
|
|
26
|
+
|
|
27
|
+
if try_download
|
|
28
|
+
puts "==> Using pre-built Ruby #{ruby_version} for #{target}"
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts "==> No pre-built binary available, building from source..."
|
|
33
|
+
build_from_source!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def try_download
|
|
39
|
+
url = "#{DOWNLOAD_BASE}/ruby-#{ruby_version}/ruby-#{ruby_version}-#{target}.tar.gz"
|
|
40
|
+
tarball = "#{output_dir}.tar.gz"
|
|
41
|
+
|
|
42
|
+
puts "==> Checking for pre-built Ruby #{ruby_version} for #{target}..."
|
|
43
|
+
|
|
44
|
+
begin
|
|
45
|
+
uri = URI(url)
|
|
46
|
+
# Follow redirects (GitHub releases redirect to S3)
|
|
47
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
|
|
48
|
+
open_timeout: 5, read_timeout: 30) do |http|
|
|
49
|
+
request = Net::HTTP::Head.new(uri)
|
|
50
|
+
http.request(request)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
54
|
+
puts " Not found (#{response.code})"
|
|
55
|
+
return false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts " Downloading..."
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(tarball))
|
|
60
|
+
|
|
61
|
+
# Stream download
|
|
62
|
+
download_file(url, tarball)
|
|
63
|
+
|
|
64
|
+
puts " Extracting..."
|
|
65
|
+
FileUtils.mkdir_p(output_dir)
|
|
66
|
+
system("tar", "xzf", tarball, "-C", output_dir, exception: true)
|
|
67
|
+
FileUtils.rm_f(tarball)
|
|
68
|
+
|
|
69
|
+
if built?
|
|
70
|
+
puts " Ruby #{ruby_version} ready at #{output_dir}"
|
|
71
|
+
return true
|
|
72
|
+
else
|
|
73
|
+
puts " Download extracted but Ruby binary not found, falling back to source build"
|
|
74
|
+
FileUtils.rm_rf(output_dir)
|
|
75
|
+
return false
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
puts " Download failed (#{e.message}), will build from source"
|
|
79
|
+
FileUtils.rm_f(tarball)
|
|
80
|
+
return false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def download_file(url, dest)
|
|
85
|
+
uri = URI(url)
|
|
86
|
+
max_redirects = 5
|
|
87
|
+
|
|
88
|
+
max_redirects.times do
|
|
89
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
90
|
+
read_timeout: 120) do |http|
|
|
91
|
+
request = Net::HTTP::Get.new(uri)
|
|
92
|
+
http.request(request) do |response|
|
|
93
|
+
case response
|
|
94
|
+
when Net::HTTPRedirection
|
|
95
|
+
uri = URI(response["location"])
|
|
96
|
+
next
|
|
97
|
+
when Net::HTTPSuccess
|
|
98
|
+
File.open(dest, "wb") do |f|
|
|
99
|
+
response.read_body { |chunk| f.write(chunk) }
|
|
100
|
+
end
|
|
101
|
+
return
|
|
102
|
+
else
|
|
103
|
+
raise "HTTP #{response.code}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
raise "Too many redirects"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_from_source!
|
|
113
|
+
Rubox.validate_data_dir!
|
|
114
|
+
script = File.join(Rubox.data_dir, "scripts", "build-ruby.sh")
|
|
115
|
+
|
|
116
|
+
env = {
|
|
117
|
+
"RUBOX_DATA_DIR" => Rubox.data_dir,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cmd = [
|
|
121
|
+
script,
|
|
122
|
+
"--ruby-version", ruby_version,
|
|
123
|
+
"--target", target,
|
|
124
|
+
"--output", output_dir,
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
system(env, *cmd, exception: true)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/rubox/cli.rb
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require "rubox"
|
|
3
|
+
|
|
4
|
+
module Rubox
|
|
5
|
+
class CLI
|
|
6
|
+
def self.run(argv)
|
|
7
|
+
new(argv).run
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(argv)
|
|
11
|
+
@argv = argv.dup
|
|
12
|
+
@options = {
|
|
13
|
+
prune: "default",
|
|
14
|
+
yes: false,
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
command = @argv.shift
|
|
20
|
+
|
|
21
|
+
case command
|
|
22
|
+
when "pack"
|
|
23
|
+
parse_pack_options!
|
|
24
|
+
cmd_pack
|
|
25
|
+
when "build-ruby"
|
|
26
|
+
parse_build_options!
|
|
27
|
+
cmd_build_ruby
|
|
28
|
+
when "clean"
|
|
29
|
+
cmd_clean
|
|
30
|
+
when "-h", "--help", "help", nil
|
|
31
|
+
print_usage
|
|
32
|
+
when "-v", "--version"
|
|
33
|
+
puts "rubox #{VERSION}"
|
|
34
|
+
else
|
|
35
|
+
$stderr.puts "Unknown command: #{command}"
|
|
36
|
+
$stderr.puts "Run: rubox --help"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_pack_options!
|
|
44
|
+
OptionParser.new do |opts|
|
|
45
|
+
opts.banner = "Usage: rubox pack [options]"
|
|
46
|
+
opts.on("--gem NAME", "Package a rubygems gem") { |v| @options[:gem] = v }
|
|
47
|
+
opts.on("--gemfile PATH", "Package a Gemfile-based app") { |v| @options[:gemfile] = v }
|
|
48
|
+
opts.on("--entry BIN", "Entry point binary name") { |v| @options[:entry] = v }
|
|
49
|
+
opts.on("--target TARGET", "Target platform") { |v| @options[:target] = v }
|
|
50
|
+
opts.on("--output PATH", "Output binary path") { |v| @options[:output] = v }
|
|
51
|
+
opts.on("--prune LEVEL", "Prune level: none, default") { |v| @options[:prune] = v }
|
|
52
|
+
opts.on("--keep-gems LIST", "Comma-separated gems to keep") { |v| @options[:keep_gems] = v }
|
|
53
|
+
opts.on("-y", "--yes", "Skip confirmation prompts") { @options[:yes] = true }
|
|
54
|
+
end.parse!(@argv)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_build_options!
|
|
58
|
+
OptionParser.new do |opts|
|
|
59
|
+
opts.banner = "Usage: rubox build-ruby [options]"
|
|
60
|
+
opts.on("--target TARGET", "Target platform") { |v| @options[:target] = v }
|
|
61
|
+
opts.on("--ruby-version VER", "Ruby version") { |v| @options[:ruby_version] = v }
|
|
62
|
+
opts.on("-y", "--yes", "Skip confirmation prompts") { @options[:yes] = true }
|
|
63
|
+
end.parse!(@argv)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cmd_pack
|
|
67
|
+
detector = Detector.new
|
|
68
|
+
|
|
69
|
+
if detector.has_config?
|
|
70
|
+
puts "Using config: #{Detector::CONFIG_FILE}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Merge: CLI flags > .rubox.yml > auto-detection
|
|
74
|
+
target = @options[:target] || detector.target || Platform.host_target
|
|
75
|
+
gem_name = @options[:gem] || detector.gem_name
|
|
76
|
+
gemfile = @options[:gemfile]
|
|
77
|
+
|
|
78
|
+
if gem_name.nil? && gemfile.nil?
|
|
79
|
+
gemfile = detector.gemfile_path
|
|
80
|
+
if gemfile
|
|
81
|
+
puts "Detected Gemfile: #{gemfile}"
|
|
82
|
+
else
|
|
83
|
+
$stderr.puts "ERROR: No --gem or --gemfile specified and no Gemfile found in #{Dir.pwd}"
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
ruby_version = @options[:ruby_version] || detector.ruby_version
|
|
89
|
+
entry = @options[:entry] || (gem_name if gem_name) || detector.entry_name
|
|
90
|
+
|
|
91
|
+
# Output path
|
|
92
|
+
output = @options[:output] || begin
|
|
93
|
+
name = entry || gem_name || File.basename(Dir.pwd)
|
|
94
|
+
suffix = (target != Platform.host_target) ? "-#{target}" : ""
|
|
95
|
+
"build/#{name}#{suffix}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
ruby_dir = "build/ruby-#{ruby_version}-#{target}"
|
|
99
|
+
|
|
100
|
+
# Build Ruby if needed
|
|
101
|
+
builder = Builder.new(ruby_version: ruby_version, target: target, output_dir: ruby_dir)
|
|
102
|
+
|
|
103
|
+
unless builder.built?
|
|
104
|
+
puts ""
|
|
105
|
+
puts "rubox needs to fetch and compile a static Ruby #{ruby_version} for #{target}."
|
|
106
|
+
puts "This is a one-time operation (cached for future builds)."
|
|
107
|
+
puts ""
|
|
108
|
+
|
|
109
|
+
unless @options[:yes]
|
|
110
|
+
print "Continue? [y/N] "
|
|
111
|
+
answer = $stdin.gets&.strip&.downcase
|
|
112
|
+
unless answer == "y" || answer == "yes"
|
|
113
|
+
puts "Aborted."
|
|
114
|
+
exit 0
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
builder.build!
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Install gem if needed
|
|
122
|
+
packager = Packager.new(
|
|
123
|
+
ruby_dir: ruby_dir,
|
|
124
|
+
output: output,
|
|
125
|
+
target: target,
|
|
126
|
+
gem_name: gem_name,
|
|
127
|
+
gemfile: gemfile,
|
|
128
|
+
entry: entry,
|
|
129
|
+
prune: @options[:prune] || detector.prune || "default",
|
|
130
|
+
keep_gems: @options[:keep_gems] || detector.keep_gems,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if gem_name
|
|
134
|
+
gem_found = Dir.glob(File.join(ruby_dir, "lib/ruby/gems/*/gems/#{gem_name}-*")).any?
|
|
135
|
+
unless gem_found
|
|
136
|
+
packager.install_gem!(gem_name)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
packager.package!
|
|
141
|
+
|
|
142
|
+
puts ""
|
|
143
|
+
puts "Binary ready: #{output}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def cmd_build_ruby
|
|
147
|
+
detector = Detector.new
|
|
148
|
+
ruby_version = @options[:ruby_version] || detector.ruby_version
|
|
149
|
+
target = @options[:target]
|
|
150
|
+
|
|
151
|
+
builder = Builder.new(ruby_version: ruby_version, target: target)
|
|
152
|
+
|
|
153
|
+
if builder.built?
|
|
154
|
+
puts "Ruby #{ruby_version} for #{target} already built at #{builder.output_dir}"
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
puts ""
|
|
159
|
+
puts "rubox needs to fetch and compile a static Ruby #{ruby_version} for #{target}."
|
|
160
|
+
puts "This is a one-time operation."
|
|
161
|
+
puts ""
|
|
162
|
+
|
|
163
|
+
unless @options[:yes]
|
|
164
|
+
print "Continue? [y/N] "
|
|
165
|
+
answer = $stdin.gets&.strip&.downcase
|
|
166
|
+
unless answer == "y" || answer == "yes"
|
|
167
|
+
puts "Aborted."
|
|
168
|
+
exit 0
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
builder.build!
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def cmd_clean
|
|
176
|
+
puts "Cleaning build artifacts..."
|
|
177
|
+
FileUtils.rm_rf("build")
|
|
178
|
+
puts "Done."
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def print_usage
|
|
182
|
+
puts <<~USAGE
|
|
183
|
+
rubox v#{VERSION} - Package Ruby apps into single portable binaries.
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
rubox pack [options] Package a gem or app into a single binary
|
|
187
|
+
rubox build-ruby [options] Build the static Ruby interpreter
|
|
188
|
+
rubox clean Remove build artifacts
|
|
189
|
+
rubox --version Show version
|
|
190
|
+
|
|
191
|
+
Pack options:
|
|
192
|
+
--gem NAME Package a rubygems gem
|
|
193
|
+
--gemfile PATH Package a Gemfile-based app (auto-detected if present)
|
|
194
|
+
--entry BIN Entry point binary name (defaults to gem name)
|
|
195
|
+
--target TARGET Target platform (default: #{Platform.host_target})
|
|
196
|
+
--output PATH Output binary path
|
|
197
|
+
--prune LEVEL Prune level: none, default (default: default)
|
|
198
|
+
--keep-gems LIST Comma-separated gems to keep despite prune list
|
|
199
|
+
-y, --yes Skip confirmation prompts
|
|
200
|
+
|
|
201
|
+
Targets:
|
|
202
|
+
aarch64-darwin macOS Apple Silicon
|
|
203
|
+
x86_64-darwin macOS Intel
|
|
204
|
+
aarch64-linux Linux ARM64
|
|
205
|
+
x86_64-linux Linux x86_64
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
rubox pack --gem herb
|
|
209
|
+
rubox pack # auto-detects Gemfile
|
|
210
|
+
rubox pack -y --target aarch64-linux
|
|
211
|
+
USAGE
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Rubox
|
|
4
|
+
class Detector
|
|
5
|
+
CONFIG_FILE = ".rubox.yml"
|
|
6
|
+
|
|
7
|
+
attr_reader :dir
|
|
8
|
+
|
|
9
|
+
def initialize(dir = Dir.pwd)
|
|
10
|
+
@dir = dir
|
|
11
|
+
@config = load_config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def config
|
|
15
|
+
@config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ruby_version
|
|
19
|
+
@config["ruby_version"] || from_ruby_version_file || from_gemfile || current_ruby_version
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def gemfile_path
|
|
23
|
+
if @config["gemfile"]
|
|
24
|
+
path = File.expand_path(@config["gemfile"], dir)
|
|
25
|
+
return path if File.exist?(path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
%w[Gemfile gems.rb].each do |name|
|
|
29
|
+
path = File.join(dir, name)
|
|
30
|
+
return path if File.exist?(path)
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def entry_name
|
|
36
|
+
return @config["entry"] if @config["entry"]
|
|
37
|
+
|
|
38
|
+
gemspec = Dir.glob(File.join(dir, "*.gemspec")).first
|
|
39
|
+
if gemspec
|
|
40
|
+
content = File.read(gemspec)
|
|
41
|
+
if content =~ /spec\.executables\s*=.*?["']([^"']+)["']/
|
|
42
|
+
return $1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
File.basename(dir)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def target
|
|
50
|
+
@config["target"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prune
|
|
54
|
+
@config["prune"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def keep_gems
|
|
58
|
+
gems = @config["keep_gems"]
|
|
59
|
+
gems.is_a?(Array) ? gems.join(",") : gems
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def gem_name
|
|
63
|
+
@config["gem"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def has_config?
|
|
67
|
+
File.exist?(File.join(dir, CONFIG_FILE))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def load_config
|
|
73
|
+
path = File.join(dir, CONFIG_FILE)
|
|
74
|
+
return {} unless File.exist?(path)
|
|
75
|
+
|
|
76
|
+
config = YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
|
|
77
|
+
config.transform_keys(&:to_s)
|
|
78
|
+
rescue => e
|
|
79
|
+
$stderr.puts "rubox: warning: failed to parse #{CONFIG_FILE}: #{e.message}"
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def from_ruby_version_file
|
|
84
|
+
path = File.join(dir, ".ruby-version")
|
|
85
|
+
return nil unless File.exist?(path)
|
|
86
|
+
|
|
87
|
+
version = File.read(path).strip.sub(/^ruby-/, "")
|
|
88
|
+
version.match?(/^\d+\.\d+\.\d+$/) ? version : nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def from_gemfile
|
|
92
|
+
path = gemfile_path
|
|
93
|
+
return nil unless path
|
|
94
|
+
|
|
95
|
+
content = File.read(path)
|
|
96
|
+
if content =~ /^\s*ruby\s+["'](\d+\.\d+\.\d+)["']/
|
|
97
|
+
$1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def current_ruby_version
|
|
102
|
+
RUBY_VERSION
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Rubox
|
|
2
|
+
class Packager
|
|
3
|
+
def initialize(ruby_dir:, output:, target:, gem_name: nil, gemfile: nil,
|
|
4
|
+
entry: nil, prune: "default", keep_gems: nil)
|
|
5
|
+
@ruby_dir = ruby_dir
|
|
6
|
+
@output = output
|
|
7
|
+
@target = target
|
|
8
|
+
@gem_name = gem_name
|
|
9
|
+
@gemfile = gemfile
|
|
10
|
+
@entry = entry || gem_name
|
|
11
|
+
@prune = prune
|
|
12
|
+
@keep_gems = keep_gems
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def package!
|
|
16
|
+
Rubox.validate_data_dir!
|
|
17
|
+
script = File.join(Rubox.data_dir, "scripts", "package.sh")
|
|
18
|
+
stub = stub_path
|
|
19
|
+
ensure_write_footer!
|
|
20
|
+
|
|
21
|
+
args = [
|
|
22
|
+
script,
|
|
23
|
+
"--ruby-dir", @ruby_dir,
|
|
24
|
+
"--stub", stub,
|
|
25
|
+
"--output", @output,
|
|
26
|
+
"--prune", @prune,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if @gem_name
|
|
30
|
+
args += ["--gem", @gem_name]
|
|
31
|
+
args += ["--entry", @entry] if @entry
|
|
32
|
+
elsif @gemfile
|
|
33
|
+
args += ["--gemfile", @gemfile]
|
|
34
|
+
args += ["--entry", @entry] if @entry
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
args += ["--keep-gems", @keep_gems] if @keep_gems
|
|
38
|
+
|
|
39
|
+
env = {
|
|
40
|
+
"RUBOX_DATA_DIR" => Rubox.data_dir,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
system(env, *args, exception: true)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def install_gem!(gem_name)
|
|
47
|
+
ruby_bin = File.join(@ruby_dir, "bin", "ruby")
|
|
48
|
+
|
|
49
|
+
if File.exist?(ruby_bin) && system(ruby_bin, "--version", out: File::NULL, err: File::NULL)
|
|
50
|
+
# Native platform -- run gem install directly
|
|
51
|
+
gem_cmd = File.join(@ruby_dir, "bin", "gem")
|
|
52
|
+
system(gem_cmd, "install", gem_name, "--no-document", exception: true)
|
|
53
|
+
else
|
|
54
|
+
# Cross-platform -- use Docker
|
|
55
|
+
puts "Installing #{gem_name} via Docker (cross-platform)..."
|
|
56
|
+
docker_platform = @target.start_with?("x86_64") ? "linux/amd64" : "linux/arm64"
|
|
57
|
+
system(
|
|
58
|
+
"docker", "run", "--rm", "--platform", docker_platform,
|
|
59
|
+
"-v", "#{File.expand_path(@ruby_dir)}:/opt/ruby",
|
|
60
|
+
"alpine:3.21",
|
|
61
|
+
"sh", "-c",
|
|
62
|
+
"apk add --no-cache build-base libgcc >/dev/null 2>&1 && /opt/ruby/bin/gem install #{gem_name} --no-document",
|
|
63
|
+
exception: true
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Find a pre-built stub for the target, or compile one from source.
|
|
71
|
+
def stub_path
|
|
72
|
+
prebuilt = File.join(Rubox.data_dir, "stubs", "stub-#{@target}")
|
|
73
|
+
return prebuilt if File.exist?(prebuilt)
|
|
74
|
+
|
|
75
|
+
# No pre-built stub -- compile from source
|
|
76
|
+
stub = File.join("build", "stub-#{@target}")
|
|
77
|
+
unless File.exist?(stub)
|
|
78
|
+
Dir.mkdir("build") unless Dir.exist?("build")
|
|
79
|
+
src = File.join(Rubox.data_dir, "ext", "stub.c")
|
|
80
|
+
|
|
81
|
+
if Platform.cross_build?(@target)
|
|
82
|
+
puts "Cross-compiling stub for #{@target} via Docker..."
|
|
83
|
+
docker_platform = @target.start_with?("x86_64") ? "linux/amd64" : "linux/arm64"
|
|
84
|
+
system(
|
|
85
|
+
"docker", "run", "--rm", "--platform", docker_platform,
|
|
86
|
+
"-v", "#{Dir.pwd}:/src", "-w", "/src",
|
|
87
|
+
"alpine:3.21",
|
|
88
|
+
"sh", "-c",
|
|
89
|
+
"apk add --no-cache gcc musl-dev >/dev/null 2>&1 && " \
|
|
90
|
+
"cc -O2 -Wall -Wextra -static -o #{stub} #{src}",
|
|
91
|
+
exception: true
|
|
92
|
+
)
|
|
93
|
+
else
|
|
94
|
+
system("cc", "-O2", "-Wall", "-Wextra", "-o", stub, src, exception: true)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
stub
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ensure_write_footer!
|
|
101
|
+
prebuilt = File.join(Rubox.data_dir, "stubs", "write-footer-#{@target}")
|
|
102
|
+
if File.exist?(prebuilt)
|
|
103
|
+
# Symlink pre-built into build/ so package.sh can find it
|
|
104
|
+
Dir.mkdir("build") unless Dir.exist?("build")
|
|
105
|
+
wf = File.join("build", "write-footer")
|
|
106
|
+
FileUtils.cp(prebuilt, wf) unless File.exist?(wf)
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
wf = File.join("build", "write-footer")
|
|
111
|
+
return if File.exist?(wf)
|
|
112
|
+
|
|
113
|
+
Dir.mkdir("build") unless Dir.exist?("build")
|
|
114
|
+
src = File.join(Rubox.data_dir, "ext", "write-footer.c")
|
|
115
|
+
system("cc", "-O2", "-Wall", "-Wextra", "-o", wf, src, exception: true)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Rubox
|
|
2
|
+
module Platform
|
|
3
|
+
def self.host_arch
|
|
4
|
+
arch = RbConfig::CONFIG["host_cpu"] || `uname -m`.strip
|
|
5
|
+
case arch
|
|
6
|
+
when /arm64|aarch64/ then "aarch64"
|
|
7
|
+
when /x86_64|amd64/ then "x86_64"
|
|
8
|
+
else arch
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.host_os
|
|
13
|
+
case RbConfig::CONFIG["host_os"]
|
|
14
|
+
when /darwin/ then "darwin"
|
|
15
|
+
when /linux/ then "linux"
|
|
16
|
+
else `uname -s`.strip.downcase
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.host_target
|
|
21
|
+
"#{host_arch}-#{host_os}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.valid_targets
|
|
25
|
+
%w[aarch64-darwin x86_64-darwin aarch64-linux x86_64-linux]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.valid_target?(target)
|
|
29
|
+
valid_targets.include?(target)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.cross_build?(target)
|
|
33
|
+
target != host_target
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.linux_target?(target)
|
|
37
|
+
target.end_with?("-linux")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/rubox.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "rubox/version"
|
|
3
|
+
require "rubox/platform"
|
|
4
|
+
require "rubox/detector"
|
|
5
|
+
require "rubox/builder"
|
|
6
|
+
require "rubox/packager"
|
|
7
|
+
|
|
8
|
+
module Rubox
|
|
9
|
+
def self.data_dir
|
|
10
|
+
@data_dir ||= File.expand_path("../data", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
REQUIRED_DATA_FILES = %w[
|
|
14
|
+
scripts/build-ruby.sh
|
|
15
|
+
scripts/package.sh
|
|
16
|
+
scripts/_common.sh
|
|
17
|
+
ext/stub.c
|
|
18
|
+
ext/write-footer.c
|
|
19
|
+
Dockerfile.ruby-build
|
|
20
|
+
prune-list.conf
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def self.validate_data_dir!
|
|
24
|
+
missing = REQUIRED_DATA_FILES.select { |f| !File.exist?(File.join(data_dir, f)) }
|
|
25
|
+
return if missing.empty?
|
|
26
|
+
|
|
27
|
+
abort "rubox: data directory incomplete (#{data_dir}).\n" \
|
|
28
|
+
"Missing: #{missing.join(', ')}\n" \
|
|
29
|
+
"Reinstall the gem: gem install rubox"
|
|
30
|
+
end
|
|
31
|
+
end
|