kompo 0.2.0 → 0.3.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/.devcontainer/Dockerfile +2 -4
- data/.devcontainer/devcontainer.json +2 -2
- data/.standard.yml +2 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +115 -3
- data/README.md +117 -10
- data/Rakefile +12 -2
- data/docs/document.md.ja +31 -0
- data/exe/kompo +45 -14
- data/lib/fs.c.erb +6 -0
- data/lib/kompo/cache.rb +65 -0
- data/lib/kompo/kompo_ignore.rb +40 -0
- data/lib/kompo/tasks/build_native_gem.rb +191 -0
- data/lib/kompo/tasks/bundle_install.rb +224 -0
- data/lib/kompo/tasks/cargo_path.rb +59 -0
- data/lib/kompo/tasks/check_stdlibs.rb +58 -0
- data/lib/kompo/tasks/collect_dependencies.rb +101 -0
- data/lib/kompo/tasks/copy_gemfile.rb +46 -0
- data/lib/kompo/tasks/copy_project_files.rb +89 -0
- data/lib/kompo/tasks/find_native_extensions.rb +89 -0
- data/lib/kompo/tasks/homebrew.rb +83 -0
- data/lib/kompo/tasks/install_deps.rb +365 -0
- data/lib/kompo/tasks/install_ruby.rb +427 -0
- data/lib/kompo/tasks/kompo_vfs_path.rb +144 -0
- data/lib/kompo/tasks/kompo_vfs_version_check.rb +56 -0
- data/lib/kompo/tasks/make_fs_c.rb +202 -0
- data/lib/kompo/tasks/make_main_c.rb +65 -0
- data/lib/kompo/tasks/packing.rb +235 -0
- data/lib/kompo/tasks/ruby_build_path.rb +54 -0
- data/lib/kompo/tasks/work_dir.rb +84 -0
- data/lib/kompo/version.rb +2 -1
- data/lib/kompo.rb +47 -420
- data/lib/main.c.erb +28 -15
- data/rbs_collection.lock.yaml +116 -0
- data/rbs_collection.yaml +19 -0
- metadata +72 -8
- data/lib/kompo/kompo_fs.rb +0 -15
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'find'
|
|
6
|
+
|
|
7
|
+
module Kompo
|
|
8
|
+
# Struct to hold file data for embedding
|
|
9
|
+
KompoFile = Struct.new(:path, :bytes)
|
|
10
|
+
|
|
11
|
+
# Generate fs.c containing embedded file data
|
|
12
|
+
# Required context:
|
|
13
|
+
# - gems: Array of gem paths to embed
|
|
14
|
+
# - ruby_std_libs: Array of Ruby standard library paths to embed
|
|
15
|
+
# - bundler_config: Path to .bundle/config (if using Gemfile)
|
|
16
|
+
# Dependencies provide:
|
|
17
|
+
# - WorkDir.path
|
|
18
|
+
# - CopyProjectFiles.entrypoint_path
|
|
19
|
+
class MakeFsC < Taski::Task
|
|
20
|
+
exports :path
|
|
21
|
+
|
|
22
|
+
# File extensions to skip when embedding
|
|
23
|
+
SKIP_EXTENSIONS = %w[
|
|
24
|
+
.so .bundle .c .h .o .java .jar .gz .dat .sqlite3 .exe
|
|
25
|
+
.gem .out .png .jpg .jpeg .gif .bmp .ico .svg .webp .ttf .data
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# Directory names to prune when traversing
|
|
29
|
+
PRUNE_DIRS = %w[.git ports logs spec .github docs exe _ruby].freeze
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
@work_dir = WorkDir.path
|
|
33
|
+
@path = File.join(@work_dir, 'fs.c')
|
|
34
|
+
|
|
35
|
+
# Get original paths for Ruby standard library cache support
|
|
36
|
+
# When Ruby is restored from cache, standard library paths are from the original build
|
|
37
|
+
# We need to embed Ruby stdlib files with those original paths so the VFS can find them
|
|
38
|
+
# Project files and gems use current work_dir paths (no replacement needed)
|
|
39
|
+
@original_ruby_install_dir = InstallRuby.original_ruby_install_dir
|
|
40
|
+
@current_ruby_install_dir = InstallRuby.ruby_install_dir
|
|
41
|
+
|
|
42
|
+
# Initialize .kompoignore handler
|
|
43
|
+
project_dir = Taski.args.fetch(:project_dir, Taski.env.working_directory) || Taski.env.working_directory
|
|
44
|
+
@kompo_ignore = KompoIgnore.new(project_dir)
|
|
45
|
+
puts "Using .kompoignore from #{project_dir}" if @kompo_ignore.enabled?
|
|
46
|
+
|
|
47
|
+
@file_bytes = []
|
|
48
|
+
@paths = []
|
|
49
|
+
@file_sizes = [0]
|
|
50
|
+
|
|
51
|
+
group('Collecting files') do
|
|
52
|
+
embed_paths = collect_embed_paths
|
|
53
|
+
|
|
54
|
+
embed_paths.each do |embed_path|
|
|
55
|
+
expand_path = File.expand_path(embed_path)
|
|
56
|
+
unless File.exist?(expand_path)
|
|
57
|
+
warn "warn: #{expand_path} does not exist. Skipping."
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if File.directory?(expand_path)
|
|
62
|
+
process_directory(expand_path)
|
|
63
|
+
else
|
|
64
|
+
add_file(expand_path)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
puts "Collected #{@file_sizes.size - 1} files"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
group('Generating fs.c') do
|
|
71
|
+
context = build_template_context
|
|
72
|
+
|
|
73
|
+
template_path = File.join(__dir__, '..', '..', 'fs.c.erb')
|
|
74
|
+
template = ERB.new(File.read(template_path))
|
|
75
|
+
File.write(@path, template.result(binding))
|
|
76
|
+
puts "Generated: fs.c (#{@file_bytes.size} bytes)"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def clean
|
|
81
|
+
return unless @path && File.exist?(@path)
|
|
82
|
+
|
|
83
|
+
FileUtils.rm_f(@path)
|
|
84
|
+
puts 'Cleaned up fs.c'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def collect_embed_paths
|
|
90
|
+
paths = []
|
|
91
|
+
|
|
92
|
+
# 1. Project files (entrypoint + additional files/directories specified by user)
|
|
93
|
+
# Created by: CopyProjectFiles
|
|
94
|
+
paths << CopyProjectFiles.entrypoint_path
|
|
95
|
+
paths += CopyProjectFiles.additional_paths
|
|
96
|
+
|
|
97
|
+
# 2. Gemfile and Gemfile.lock (if exists)
|
|
98
|
+
# Created by: CopyGemfile
|
|
99
|
+
if CopyGemfile.gemfile_exists
|
|
100
|
+
paths << File.join(@work_dir, 'Gemfile')
|
|
101
|
+
paths << File.join(@work_dir, 'Gemfile.lock')
|
|
102
|
+
|
|
103
|
+
# 3. Bundle directory (.bundle/config and bundle/ruby/X.Y.Z/gems/...)
|
|
104
|
+
# Created by: BundleInstall
|
|
105
|
+
paths << BundleInstall.bundler_config_path
|
|
106
|
+
paths << BundleInstall.bundle_ruby_dir
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 4. Ruby standard library
|
|
110
|
+
# Retrieved by: CheckStdlibs
|
|
111
|
+
paths += CheckStdlibs.paths
|
|
112
|
+
|
|
113
|
+
paths.compact
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def process_directory(dir_path)
|
|
117
|
+
# Resolve base directory to ensure symlink safety
|
|
118
|
+
real_base = File.realpath(dir_path)
|
|
119
|
+
|
|
120
|
+
Find.find(dir_path) do |path|
|
|
121
|
+
# Prune certain directories
|
|
122
|
+
if File.directory?(path)
|
|
123
|
+
base = File.basename(path)
|
|
124
|
+
Find.prune if PRUNE_DIRS.any? { |d| base == d || path.end_with?("/#{d}") }
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Skip symlinks that escape the base directory (symlink traversal prevention)
|
|
129
|
+
if File.symlink?(path)
|
|
130
|
+
real_path = File.realpath(path)
|
|
131
|
+
unless real_path.start_with?(real_base)
|
|
132
|
+
warn "warn: Skipping symlink escaping base directory: #{path} -> #{real_path}"
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Skip certain file extensions
|
|
138
|
+
next if SKIP_EXTENSIONS.any? { |ext| path.end_with?(ext) }
|
|
139
|
+
next if path.end_with?('selenium-manager')
|
|
140
|
+
|
|
141
|
+
# Skip files matching .kompoignore patterns
|
|
142
|
+
next if should_ignore?(path)
|
|
143
|
+
|
|
144
|
+
add_file(path)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if a file should be ignored based on .kompoignore patterns
|
|
149
|
+
# Only applies to files under work_dir (Ruby standard library is excluded)
|
|
150
|
+
def should_ignore?(absolute_path)
|
|
151
|
+
return false unless @kompo_ignore&.enabled?
|
|
152
|
+
|
|
153
|
+
# Only apply .kompoignore to work_dir files (project files and gems)
|
|
154
|
+
# Ruby standard library paths are outside work_dir and should not be filtered
|
|
155
|
+
return false unless absolute_path.start_with?(@work_dir)
|
|
156
|
+
|
|
157
|
+
relative_path = absolute_path.sub("#{@work_dir}/", '')
|
|
158
|
+
if @kompo_ignore.ignore?(relative_path)
|
|
159
|
+
puts "Ignoring (via .kompoignore): #{relative_path}"
|
|
160
|
+
true
|
|
161
|
+
else
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def add_file(path)
|
|
167
|
+
# Keep original paths for VFS - the caching system already ensures
|
|
168
|
+
# the same work_dir path is reused across builds via metadata.json
|
|
169
|
+
# Ruby's $LOAD_PATH uses work_dir paths, so embedded files must match.
|
|
170
|
+
embedded_path = if @current_ruby_install_dir != @original_ruby_install_dir && path.start_with?(@current_ruby_install_dir)
|
|
171
|
+
# Ruby install dir path replacement for cache compatibility (when paths differ)
|
|
172
|
+
path.sub(@current_ruby_install_dir, @original_ruby_install_dir)
|
|
173
|
+
else
|
|
174
|
+
path
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
puts "#{path} -> #{embedded_path}" if path != embedded_path
|
|
178
|
+
|
|
179
|
+
# Use binread for binary-safe reading (preserves exact bytes without encoding conversion)
|
|
180
|
+
content = File.binread(path)
|
|
181
|
+
bytes = content.bytes
|
|
182
|
+
byte_size = content.bytesize
|
|
183
|
+
path_bytes = embedded_path.bytes << 0 # null-terminated
|
|
184
|
+
|
|
185
|
+
file = KompoFile.new(path_bytes, bytes)
|
|
186
|
+
|
|
187
|
+
@file_bytes.concat(file.bytes)
|
|
188
|
+
@paths.concat(file.path)
|
|
189
|
+
prev_size = @file_sizes.last
|
|
190
|
+
@file_sizes << (prev_size + byte_size)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def build_template_context
|
|
194
|
+
FsCTemplateContext.new(
|
|
195
|
+
work_dir: @work_dir
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Struct for fs.c template variables
|
|
200
|
+
FsCTemplateContext = Struct.new(:work_dir, keyword_init: true)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Kompo
|
|
7
|
+
# Generate main.c from ERB template
|
|
8
|
+
# Required context:
|
|
9
|
+
# - exts: Array of [so_path, init_func] pairs from native gem builds
|
|
10
|
+
# Dependencies provide:
|
|
11
|
+
# - WorkDir.path
|
|
12
|
+
# - CopyProjectFiles.entrypoint_path
|
|
13
|
+
class MakeMainC < Taski::Task
|
|
14
|
+
exports :path
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
work_dir = WorkDir.path
|
|
18
|
+
@path = File.join(work_dir, 'main.c')
|
|
19
|
+
|
|
20
|
+
return if File.exist?(@path)
|
|
21
|
+
|
|
22
|
+
template_path = File.join(__dir__, '..', '..', 'main.c.erb')
|
|
23
|
+
template = ERB.new(File.read(template_path))
|
|
24
|
+
|
|
25
|
+
# Build context object for ERB template
|
|
26
|
+
context = build_template_context
|
|
27
|
+
|
|
28
|
+
File.write(@path, template.result(binding))
|
|
29
|
+
puts 'Generated: main.c'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clean
|
|
33
|
+
return unless @path && File.exist?(@path)
|
|
34
|
+
|
|
35
|
+
FileUtils.rm_f(@path)
|
|
36
|
+
puts 'Cleaned up main.c'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_template_context
|
|
42
|
+
project_dir = Taski.args.fetch(:project_dir, Taski.env.working_directory) || Taski.env.working_directory
|
|
43
|
+
work_dir = WorkDir.path
|
|
44
|
+
entrypoint = CopyProjectFiles.entrypoint_path
|
|
45
|
+
|
|
46
|
+
TemplateContext.new(
|
|
47
|
+
exts: BuildNativeGem.exts || [],
|
|
48
|
+
work_dir: work_dir,
|
|
49
|
+
work_dir_entrypoint: entrypoint,
|
|
50
|
+
project_dir: project_dir,
|
|
51
|
+
has_gemfile: CopyGemfile.gemfile_exists
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Simple struct to hold template variables
|
|
56
|
+
TemplateContext = Struct.new(
|
|
57
|
+
:exts,
|
|
58
|
+
:work_dir,
|
|
59
|
+
:work_dir_entrypoint,
|
|
60
|
+
:project_dir,
|
|
61
|
+
:has_gemfile,
|
|
62
|
+
keyword_init: true
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'shellwords'
|
|
5
|
+
|
|
6
|
+
module Kompo
|
|
7
|
+
# Section to compile the final binary.
|
|
8
|
+
# Switches implementation based on the current platform.
|
|
9
|
+
# Uses CollectDependencies's exported values for dependencies.
|
|
10
|
+
class Packing < Taski::Section
|
|
11
|
+
interfaces :output_path
|
|
12
|
+
|
|
13
|
+
def impl
|
|
14
|
+
macos? ? ForMacOS : ForLinux
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Common helper methods shared between macOS and Linux implementations
|
|
18
|
+
module CommonHelpers
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def get_ruby_cflags(ruby_install_dir)
|
|
22
|
+
ruby_pc = File.join(ruby_install_dir, 'lib', 'pkgconfig', 'ruby.pc')
|
|
23
|
+
output, = Open3.capture2('pkg-config', '--cflags', ruby_pc, err: File::NULL)
|
|
24
|
+
Shellwords.split(output.chomp)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_ruby_mainlibs(ruby_install_dir)
|
|
28
|
+
ruby_pc = File.join(ruby_install_dir, 'lib', 'pkgconfig', 'ruby.pc')
|
|
29
|
+
output, = Open3.capture2('pkg-config', '--variable=MAINLIBS', ruby_pc, err: File::NULL)
|
|
30
|
+
output.chomp
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def get_ldflags(work_dir, ruby_major_minor)
|
|
34
|
+
makefiles = Dir.glob("#{work_dir}/bundle/ruby/#{ruby_major_minor}.0/gems/*/ext/*/Makefile")
|
|
35
|
+
flags = makefiles.flat_map do |makefile|
|
|
36
|
+
content = File.read(makefile)
|
|
37
|
+
ldflags = content.scan(/^ldflags\s+= (.*)/).flatten
|
|
38
|
+
ldflags += content.scan(/^LDFLAGS\s+= (.*)/).flatten
|
|
39
|
+
ldflags.flat_map { |f| f.split(' ') }
|
|
40
|
+
end
|
|
41
|
+
flags.uniq.select { |f| f.start_with?('-L') }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_libpath(work_dir, ruby_major_minor)
|
|
45
|
+
makefiles = Dir.glob("#{work_dir}/bundle/ruby/#{ruby_major_minor}.0/gems/*/ext/*/Makefile")
|
|
46
|
+
makefiles.flat_map do |makefile|
|
|
47
|
+
content = File.read(makefile)
|
|
48
|
+
content.scan(/^LIBPATH = (.*)/).flatten
|
|
49
|
+
end.compact.flat_map { |p| p.split(' ') }.uniq
|
|
50
|
+
.reject { |p| p.start_with?('-Wl,-rpath,') || !p.start_with?('-L/') }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_extlibs(ruby_build_path, ruby_version)
|
|
54
|
+
exts_mk_files = Dir.glob(File.join(ruby_build_path, "ruby-#{ruby_version}", 'ext', '**', 'exts.mk'))
|
|
55
|
+
exts_mk_files.flat_map do |file|
|
|
56
|
+
File.read(file).scan(/^EXTLIBS\s+= (.*)/).flatten
|
|
57
|
+
end.compact.flat_map { |l| l.split(' ') }.uniq
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get_gem_libs(work_dir, ruby_major_minor)
|
|
61
|
+
makefiles = Dir.glob("#{work_dir}/bundle/ruby/#{ruby_major_minor}.0/gems/*/ext/*/Makefile")
|
|
62
|
+
makefiles.flat_map do |makefile|
|
|
63
|
+
File.read(makefile).scan(/^LIBS = (.*)/).flatten
|
|
64
|
+
end.compact.flat_map { |l| l.split(' ') }.uniq
|
|
65
|
+
.map { |l| l.start_with?('-l') ? l : "-l#{File.basename(l, '.a').delete_prefix('lib')}" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# macOS implementation - compiles with clang and Homebrew paths
|
|
70
|
+
class ForMacOS < Taski::Task
|
|
71
|
+
include CommonHelpers
|
|
72
|
+
|
|
73
|
+
# macOS system libraries
|
|
74
|
+
SYSTEM_LIBS = %w[pthread m c].freeze
|
|
75
|
+
# macOS frameworks
|
|
76
|
+
FRAMEWORKS = %w[Foundation CoreFoundation Security].freeze
|
|
77
|
+
|
|
78
|
+
def run
|
|
79
|
+
work_dir = CollectDependencies.work_dir
|
|
80
|
+
deps = CollectDependencies.deps
|
|
81
|
+
ext_paths = CollectDependencies.ext_paths
|
|
82
|
+
enc_files = CollectDependencies.enc_files
|
|
83
|
+
@output_path = CollectDependencies.output_path
|
|
84
|
+
|
|
85
|
+
command = build_command(work_dir, deps, ext_paths, enc_files)
|
|
86
|
+
|
|
87
|
+
group('Compiling binary (macOS)') do
|
|
88
|
+
system(*command) or raise 'Failed to compile final binary'
|
|
89
|
+
puts "Binary size: #{File.size(@output_path) / 1024 / 1024} MB"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
puts "Successfully created: #{@output_path}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def build_command(work_dir, deps, ext_paths, enc_files)
|
|
98
|
+
ruby_static_lib = "-lruby.#{deps.ruby_major_minor}-static"
|
|
99
|
+
|
|
100
|
+
[
|
|
101
|
+
'clang',
|
|
102
|
+
'-O3',
|
|
103
|
+
get_ruby_cflags(deps.ruby_install_dir),
|
|
104
|
+
# IMPORTANT: kompo_lib must come FIRST to override Homebrew-installed versions
|
|
105
|
+
"-L#{deps.kompo_lib}",
|
|
106
|
+
get_ldflags(work_dir, deps.ruby_major_minor),
|
|
107
|
+
"-L#{deps.ruby_lib}",
|
|
108
|
+
# Also add build path for static library lookup
|
|
109
|
+
"-L#{File.join(deps.ruby_build_path, "ruby-#{deps.ruby_version}")}",
|
|
110
|
+
# Add library paths for dependencies (Homebrew on macOS)
|
|
111
|
+
Shellwords.split(deps.deps_lib_paths),
|
|
112
|
+
get_libpath(work_dir, deps.ruby_major_minor),
|
|
113
|
+
'-fstack-protector-strong',
|
|
114
|
+
'-Wl,-dead_strip', # Remove unused code/data
|
|
115
|
+
'-Wl,-no_deduplicate', # Allow duplicate symbols from Ruby YJIT and kompo-vfs
|
|
116
|
+
'-Wl,-export_dynamic', # Export symbols to dynamic symbol table
|
|
117
|
+
deps.main_c,
|
|
118
|
+
deps.fs_c,
|
|
119
|
+
# Link kompo_wrap FIRST (before Ruby) to override libc symbols
|
|
120
|
+
'-lkompo_wrap',
|
|
121
|
+
ext_paths,
|
|
122
|
+
enc_files,
|
|
123
|
+
ruby_static_lib,
|
|
124
|
+
get_libs(deps.ruby_install_dir, work_dir, deps.ruby_build_path, deps.ruby_version, deps.ruby_major_minor),
|
|
125
|
+
'-o', @output_path
|
|
126
|
+
].flatten
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def get_libs(ruby_install_dir, work_dir, ruby_build_path, ruby_version, ruby_major_minor)
|
|
130
|
+
main_libs = get_ruby_mainlibs(ruby_install_dir)
|
|
131
|
+
ruby_std_gem_libs = get_extlibs(ruby_build_path, ruby_version)
|
|
132
|
+
gem_libs = get_gem_libs(work_dir, ruby_major_minor)
|
|
133
|
+
|
|
134
|
+
all_libs = [main_libs.split(' '), gem_libs, ruby_std_gem_libs].flatten
|
|
135
|
+
.select { |l| l.match?(/-l\w/) }.uniq
|
|
136
|
+
.reject { |l| l == '-ldl' } # macOS doesn't have libdl
|
|
137
|
+
|
|
138
|
+
# Separate system libs from other libs
|
|
139
|
+
other_libs = all_libs.reject { |l| SYSTEM_LIBS.any? { |sys| l == "-l#{sys}" } }
|
|
140
|
+
|
|
141
|
+
[
|
|
142
|
+
other_libs,
|
|
143
|
+
'-lkompo_fs',
|
|
144
|
+
# System libraries
|
|
145
|
+
SYSTEM_LIBS.map { |l| "-l#{l}" },
|
|
146
|
+
# Frameworks
|
|
147
|
+
FRAMEWORKS.flat_map { |f| ['-framework', f] }
|
|
148
|
+
].flatten
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Linux implementation - compiles with gcc and pkg-config paths
|
|
153
|
+
class ForLinux < Taski::Task
|
|
154
|
+
include CommonHelpers
|
|
155
|
+
|
|
156
|
+
# Libraries that must be dynamically linked
|
|
157
|
+
DYN_LINK_LIBS = %w[pthread dl m c].freeze
|
|
158
|
+
|
|
159
|
+
def run
|
|
160
|
+
work_dir = CollectDependencies.work_dir
|
|
161
|
+
deps = CollectDependencies.deps
|
|
162
|
+
ext_paths = CollectDependencies.ext_paths
|
|
163
|
+
enc_files = CollectDependencies.enc_files
|
|
164
|
+
@output_path = CollectDependencies.output_path
|
|
165
|
+
|
|
166
|
+
command = build_command(work_dir, deps, ext_paths, enc_files)
|
|
167
|
+
|
|
168
|
+
group('Compiling binary (Linux)') do
|
|
169
|
+
system(*command) or raise 'Failed to compile final binary'
|
|
170
|
+
puts "Binary size: #{File.size(@output_path) / 1024 / 1024} MB"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
puts "Successfully created: #{@output_path}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def build_command(work_dir, deps, ext_paths, enc_files)
|
|
179
|
+
# Linux uses libruby-static.a (not libruby.X.Y-static.a like macOS)
|
|
180
|
+
ruby_static_lib = '-lruby-static'
|
|
181
|
+
|
|
182
|
+
[
|
|
183
|
+
'gcc',
|
|
184
|
+
'-O3',
|
|
185
|
+
'-no-pie', # Required: Rust std lib is not built with PIC
|
|
186
|
+
get_ruby_cflags(deps.ruby_install_dir),
|
|
187
|
+
# IMPORTANT: kompo_lib must come FIRST to override system-installed versions
|
|
188
|
+
"-L#{deps.kompo_lib}",
|
|
189
|
+
get_ldflags(work_dir, deps.ruby_major_minor),
|
|
190
|
+
"-L#{deps.ruby_lib}",
|
|
191
|
+
# Also add build path for static library lookup
|
|
192
|
+
"-L#{File.join(deps.ruby_build_path, "ruby-#{deps.ruby_version}")}",
|
|
193
|
+
# Add library paths for dependencies (from pkg-config)
|
|
194
|
+
Shellwords.split(deps.deps_lib_paths),
|
|
195
|
+
get_libpath(work_dir, deps.ruby_major_minor),
|
|
196
|
+
'-fstack-protector-strong',
|
|
197
|
+
'-rdynamic', '-Wl,-export-dynamic',
|
|
198
|
+
deps.main_c,
|
|
199
|
+
deps.fs_c,
|
|
200
|
+
'-Wl,-Bstatic',
|
|
201
|
+
'-Wl,--start-group',
|
|
202
|
+
ext_paths,
|
|
203
|
+
enc_files,
|
|
204
|
+
ruby_static_lib,
|
|
205
|
+
get_libs(deps.ruby_install_dir, work_dir, deps.ruby_build_path, deps.ruby_version, deps.ruby_major_minor),
|
|
206
|
+
'-o', @output_path
|
|
207
|
+
].flatten
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def get_libs(ruby_install_dir, work_dir, ruby_build_path, ruby_version, ruby_major_minor)
|
|
211
|
+
main_libs = get_ruby_mainlibs(ruby_install_dir)
|
|
212
|
+
ruby_std_gem_libs = get_extlibs(ruby_build_path, ruby_version)
|
|
213
|
+
gem_libs = get_gem_libs(work_dir, ruby_major_minor)
|
|
214
|
+
|
|
215
|
+
dyn_link_libs = DYN_LINK_LIBS.map { |l| "-l#{l}" }
|
|
216
|
+
|
|
217
|
+
all_libs = [main_libs.split(' '), gem_libs, ruby_std_gem_libs].flatten
|
|
218
|
+
.select { |l| l.match?(/-l\w/) }.uniq
|
|
219
|
+
|
|
220
|
+
static_libs, dyn_libs = all_libs.partition { |l| !dyn_link_libs.include?(l) }
|
|
221
|
+
|
|
222
|
+
dyn_libs << '-lc'
|
|
223
|
+
dyn_libs.unshift('-Wl,-Bdynamic')
|
|
224
|
+
|
|
225
|
+
[static_libs, '-Wl,--end-group', '-lkompo_fs', '-lkompo_wrap', dyn_libs].flatten
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def macos?
|
|
232
|
+
RUBY_PLATFORM.include?('darwin')
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Kompo
|
|
6
|
+
# Section to get the ruby-build path.
|
|
7
|
+
# Switches implementation based on whether ruby-build is already installed.
|
|
8
|
+
class RubyBuildPath < Taski::Section
|
|
9
|
+
interfaces :path
|
|
10
|
+
|
|
11
|
+
def impl
|
|
12
|
+
ruby_build_installed? ? Installed : Install
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Use existing ruby-build installation
|
|
16
|
+
class Installed < Taski::Task
|
|
17
|
+
def run
|
|
18
|
+
path_output, = Open3.capture2('which', 'ruby-build', err: File::NULL)
|
|
19
|
+
@path = path_output.chomp
|
|
20
|
+
puts "ruby-build path: #{@path}"
|
|
21
|
+
version_output, = Open3.capture2(@path, '--version', err: File::NULL)
|
|
22
|
+
puts "ruby-build version: #{version_output.chomp}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Install ruby-build via git clone and return the path
|
|
27
|
+
class Install < Taski::Task
|
|
28
|
+
def run
|
|
29
|
+
puts 'ruby-build not found. Installing via git...'
|
|
30
|
+
install_dir = File.expand_path('~/.ruby-build')
|
|
31
|
+
|
|
32
|
+
if Dir.exist?(install_dir)
|
|
33
|
+
system('git', '-C', install_dir, 'pull', '--quiet')
|
|
34
|
+
else
|
|
35
|
+
system('git', 'clone', 'https://github.com/rbenv/ruby-build.git', install_dir)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@path = File.join(install_dir, 'bin', 'ruby-build')
|
|
39
|
+
raise 'Failed to install ruby-build' unless File.executable?(@path)
|
|
40
|
+
|
|
41
|
+
puts "ruby-build installed at: #{@path}"
|
|
42
|
+
version_output, = Open3.capture2(@path, '--version', err: File::NULL)
|
|
43
|
+
puts "ruby-build version: #{version_output.chomp}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def ruby_build_installed?
|
|
50
|
+
_, status = Open3.capture2('which', 'ruby-build', err: File::NULL)
|
|
51
|
+
status.success?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
module Kompo
|
|
9
|
+
# Create a temporary working directory and change into it
|
|
10
|
+
class WorkDir < Taski::Task
|
|
11
|
+
exports :path, :original_dir
|
|
12
|
+
|
|
13
|
+
# Marker file to identify Kompo-created work directories
|
|
14
|
+
MARKER_FILE = ".kompo_work_dir_marker"
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
@original_dir = Dir.pwd
|
|
18
|
+
|
|
19
|
+
# Check if Ruby cache exists and use its work_dir path for $LOAD_PATH compatibility
|
|
20
|
+
ruby_version = Taski.args.fetch(:ruby_version, RUBY_VERSION)
|
|
21
|
+
kompo_cache = Taski.args.fetch(:kompo_cache, File.expand_path("~/.kompo/cache"))
|
|
22
|
+
cache_metadata_path = File.join(kompo_cache, ruby_version, "metadata.json")
|
|
23
|
+
|
|
24
|
+
if File.exist?(cache_metadata_path)
|
|
25
|
+
begin
|
|
26
|
+
metadata = JSON.parse(File.read(cache_metadata_path))
|
|
27
|
+
cached_work_dir = metadata["work_dir"]
|
|
28
|
+
|
|
29
|
+
if cached_work_dir
|
|
30
|
+
# Check if the directory exists and belongs to us (has marker) or doesn't exist at all
|
|
31
|
+
# In CI environments, the temp directory is cleaned between runs, so we recreate it
|
|
32
|
+
if Dir.exist?(cached_work_dir)
|
|
33
|
+
marker_path = File.join(cached_work_dir, MARKER_FILE)
|
|
34
|
+
if File.exist?(marker_path)
|
|
35
|
+
# Directory exists and has our marker - reuse it
|
|
36
|
+
@path = cached_work_dir
|
|
37
|
+
puts "Using cached work directory: #{@path}"
|
|
38
|
+
return
|
|
39
|
+
else
|
|
40
|
+
# Directory exists but wasn't created by Kompo - don't use it
|
|
41
|
+
warn "warn: #{cached_work_dir} exists but is not a Kompo work directory, creating new one"
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
# Directory doesn't exist - recreate it (common in CI after cache restore)
|
|
45
|
+
FileUtils.mkdir_p(cached_work_dir)
|
|
46
|
+
File.write(File.join(cached_work_dir, MARKER_FILE), "kompo-work-dir")
|
|
47
|
+
@path = cached_work_dir
|
|
48
|
+
puts "Recreated cached work directory: #{@path}"
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
# Fall through to create new work_dir
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# No valid cache, create new work_dir
|
|
58
|
+
tmpdir = Dir.mktmpdir(SecureRandom.uuid)
|
|
59
|
+
# Resolve symlinks to get the real path
|
|
60
|
+
# On macOS, /var/folders is a symlink to /private/var/folders
|
|
61
|
+
# If we don't resolve this, paths won't match at runtime
|
|
62
|
+
@path = File.realpath(tmpdir)
|
|
63
|
+
|
|
64
|
+
# Create marker file to identify this as a Kompo work directory
|
|
65
|
+
File.write(File.join(@path, MARKER_FILE), "kompo-work-dir")
|
|
66
|
+
|
|
67
|
+
puts "Working directory: #{@path}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def clean
|
|
71
|
+
return unless @path && Dir.exist?(@path)
|
|
72
|
+
|
|
73
|
+
# Only remove if marker file exists (confirms this is a Kompo work directory)
|
|
74
|
+
marker_path = File.join(@path, MARKER_FILE)
|
|
75
|
+
unless File.exist?(marker_path)
|
|
76
|
+
puts "Skipping cleanup: #{@path} is not a Kompo work directory"
|
|
77
|
+
return
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
FileUtils.rm_rf(@path)
|
|
81
|
+
puts "Cleaned up working directory: #{@path}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|