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,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Kompo
|
|
7
|
+
# Build native gem extensions (C extensions and Rust extensions)
|
|
8
|
+
# Exports:
|
|
9
|
+
# - exts: Array of [so_path, init_func] pairs for main.c template
|
|
10
|
+
# - exts_dir: Directory containing compiled .o files
|
|
11
|
+
class BuildNativeGem < Taski::Task
|
|
12
|
+
exports :exts, :exts_dir
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
@exts = []
|
|
16
|
+
@exts_dir = nil
|
|
17
|
+
|
|
18
|
+
extensions = FindNativeExtensions.extensions
|
|
19
|
+
if extensions.empty?
|
|
20
|
+
puts "No native extensions to build"
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
work_dir = WorkDir.path
|
|
25
|
+
@exts_dir = File.join(work_dir, "ext")
|
|
26
|
+
|
|
27
|
+
extensions.each do |ext|
|
|
28
|
+
build_extension(ext, work_dir)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts "Completed #{@exts.size} native extensions"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clean
|
|
35
|
+
return unless @exts_dir && Dir.exist?(@exts_dir)
|
|
36
|
+
|
|
37
|
+
FileUtils.rm_rf(@exts_dir)
|
|
38
|
+
puts "Cleaned up native extensions"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_extension(ext, work_dir)
|
|
44
|
+
dir_name = ext[:dir_name]
|
|
45
|
+
gem_ext_name = ext[:gem_ext_name]
|
|
46
|
+
ext_type = ext[:is_rust] ? "Rust" : "C"
|
|
47
|
+
|
|
48
|
+
group("Building #{gem_ext_name} (#{ext_type})") do
|
|
49
|
+
if ext[:is_rust]
|
|
50
|
+
build_rust_extension(dir_name, ext[:cargo_toml], gem_ext_name, work_dir)
|
|
51
|
+
else
|
|
52
|
+
build_c_extension(dir_name, gem_ext_name, work_dir)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
register_extension(dir_name, gem_ext_name)
|
|
56
|
+
puts "Built: #{gem_ext_name}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register_extension(dir_name, gem_ext_name)
|
|
61
|
+
makefile_path = File.join(dir_name, "Makefile")
|
|
62
|
+
|
|
63
|
+
if File.exist?(makefile_path)
|
|
64
|
+
# C extension: parse Makefile
|
|
65
|
+
makefile_content = File.read(makefile_path)
|
|
66
|
+
prefix = makefile_content.scan(/target_prefix = (.*)/).flatten.first&.delete_prefix("/") || ""
|
|
67
|
+
target_name = makefile_content.scan(/TARGET_NAME = (.*)/).flatten.first || gem_ext_name
|
|
68
|
+
else
|
|
69
|
+
# Rust extension: parse Cargo.toml
|
|
70
|
+
cargo_toml_path = File.join(dir_name, "Cargo.toml")
|
|
71
|
+
unless File.exist?(cargo_toml_path)
|
|
72
|
+
raise "Cannot register extension #{gem_ext_name} in #{dir_name}: " \
|
|
73
|
+
"neither Makefile nor Cargo.toml found (build_rust_extension may have produced .a files)"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
cargo_content = File.read(cargo_toml_path)
|
|
77
|
+
prefix = "" # Rust extensions typically don't have a prefix
|
|
78
|
+
target_name = parse_cargo_toml_target_name(cargo_content)
|
|
79
|
+
unless target_name
|
|
80
|
+
raise "Cannot determine target name for #{gem_ext_name} in #{dir_name}: " \
|
|
81
|
+
"Cargo.toml lacks [lib].name or [package].name"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Path for ruby_init_ext must match require path (without file extension)
|
|
86
|
+
ext_path = File.join(prefix, target_name).delete_prefix("/")
|
|
87
|
+
@exts << [ext_path, "Init_#{target_name}"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Parse Cargo.toml to extract target name
|
|
91
|
+
# Prefers [lib].name over [package].name
|
|
92
|
+
def parse_cargo_toml_target_name(content)
|
|
93
|
+
current_section = nil
|
|
94
|
+
lib_name = nil
|
|
95
|
+
package_name = nil
|
|
96
|
+
|
|
97
|
+
content.each_line do |line|
|
|
98
|
+
line = line.strip
|
|
99
|
+
|
|
100
|
+
# Match section headers like [package], [lib], etc.
|
|
101
|
+
if line =~ /^\[([^\]]+)\]$/
|
|
102
|
+
current_section = ::Regexp.last_match(1)
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Match name = "value" or name = 'value'
|
|
107
|
+
if line =~ /^name\s*=\s*["']([^"']+)["']$/
|
|
108
|
+
case current_section
|
|
109
|
+
when "lib"
|
|
110
|
+
lib_name = ::Regexp.last_match(1)
|
|
111
|
+
when "package"
|
|
112
|
+
package_name = ::Regexp.last_match(1)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Prefer [lib].name over [package].name
|
|
118
|
+
lib_name || package_name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_rust_extension(dir_name, cargo_toml, gem_ext_name, work_dir)
|
|
122
|
+
cargo = CargoPath.path
|
|
123
|
+
|
|
124
|
+
puts "Building Rust extension: #{gem_ext_name}"
|
|
125
|
+
# Use absolute path for --target-dir to ensure artifacts are placed correctly
|
|
126
|
+
target_dir = File.join(dir_name, "target")
|
|
127
|
+
command = [
|
|
128
|
+
cargo,
|
|
129
|
+
"rustc",
|
|
130
|
+
"--release",
|
|
131
|
+
"--crate-type=staticlib",
|
|
132
|
+
"--target-dir", target_dir,
|
|
133
|
+
"--manifest-path", cargo_toml
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
system(*command) or raise "Failed to build Rust extension: #{gem_ext_name}"
|
|
137
|
+
|
|
138
|
+
# Copy .a files to ext directory
|
|
139
|
+
copy_targets = Dir.glob(File.join(target_dir, "release/*.a"))
|
|
140
|
+
dest_dir = FileUtils.mkdir_p(File.join(work_dir, "ext", gem_ext_name)).first
|
|
141
|
+
FileUtils.cp(copy_targets, dest_dir)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_c_extension(dir_name, gem_ext_name, work_dir)
|
|
145
|
+
puts "Building C extension: #{gem_ext_name}"
|
|
146
|
+
|
|
147
|
+
# Run extconf.rb to generate Makefile
|
|
148
|
+
# Use system Ruby so build-time dependencies (e.g., mini_portile2) are available via Bundler
|
|
149
|
+
puts "Running extconf.rb in #{dir_name}"
|
|
150
|
+
extconf_output, status = Open3.capture2e("ruby", "extconf.rb", chdir: dir_name)
|
|
151
|
+
unless status.success?
|
|
152
|
+
warn "extconf.rb failed for #{gem_ext_name}"
|
|
153
|
+
warn "extconf.rb output:\n#{extconf_output}"
|
|
154
|
+
raise "Failed to run extconf.rb for #{gem_ext_name}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Extract OBJS from Makefile and build
|
|
158
|
+
makefile_path = File.join(dir_name, "Makefile")
|
|
159
|
+
makefile_content = File.read(makefile_path)
|
|
160
|
+
objs_match = makefile_content.match(/OBJS = (.*\.o)/)
|
|
161
|
+
return unless objs_match
|
|
162
|
+
|
|
163
|
+
# Get full extension path (prefix/target_name) for proper directory structure
|
|
164
|
+
# This ensures erb/escape and cgi/escape are stored in different directories
|
|
165
|
+
prefix = makefile_content.scan(/target_prefix = (.*)/).flatten.first&.delete_prefix("/") || ""
|
|
166
|
+
target_name = makefile_content.scan(/TARGET_NAME = (.*)/).flatten.first || gem_ext_name
|
|
167
|
+
ext_path = File.join(prefix, target_name).delete_prefix("/")
|
|
168
|
+
dest_ext_dir = File.join(work_dir, "ext", ext_path)
|
|
169
|
+
|
|
170
|
+
# Skip if already built
|
|
171
|
+
return if Dir.exist?(dest_ext_dir)
|
|
172
|
+
|
|
173
|
+
objs = objs_match[1]
|
|
174
|
+
puts "Building objects: #{objs}"
|
|
175
|
+
# Use Open3.capture2e with array form to avoid shell injection
|
|
176
|
+
make_args = ["make", "-C", dir_name] + objs.split + ["--always-make"]
|
|
177
|
+
make_output, status = Open3.capture2e(*make_args)
|
|
178
|
+
unless status.success?
|
|
179
|
+
warn "make failed for #{gem_ext_name} in #{dir_name}"
|
|
180
|
+
warn "Make output:\n#{make_output}"
|
|
181
|
+
warn "Makefile content:\n#{makefile_content[0..500]}"
|
|
182
|
+
raise "Failed to make #{gem_ext_name}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Copy .o files to ext directory
|
|
186
|
+
copy_targets = objs.split.map { |o| File.join(dir_name, o) }
|
|
187
|
+
dest_dir = FileUtils.mkdir_p(dest_ext_dir).first
|
|
188
|
+
FileUtils.cp(copy_targets, dest_dir)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'bundler'
|
|
7
|
+
|
|
8
|
+
module Kompo
|
|
9
|
+
# Shared helpers for bundle cache operations
|
|
10
|
+
module BundleCacheHelpers
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def compute_bundle_cache_name
|
|
14
|
+
hash = compute_gemfile_lock_hash
|
|
15
|
+
return nil unless hash
|
|
16
|
+
|
|
17
|
+
"bundle-#{hash}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def compute_gemfile_lock_hash
|
|
21
|
+
work_dir = WorkDir.path
|
|
22
|
+
gemfile_lock_path = File.join(work_dir, 'Gemfile.lock')
|
|
23
|
+
return nil unless File.exist?(gemfile_lock_path)
|
|
24
|
+
|
|
25
|
+
content = File.read(gemfile_lock_path)
|
|
26
|
+
Digest::SHA256.hexdigest(content)[0..15]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Run bundle install --path bundle in work directory
|
|
31
|
+
# Uses standard Bundler (not standalone mode) so Bundler.require works
|
|
32
|
+
# Supports caching based on Gemfile.lock hash and Ruby version
|
|
33
|
+
class BundleInstall < Taski::Section
|
|
34
|
+
include BundleCacheHelpers
|
|
35
|
+
|
|
36
|
+
interfaces :bundle_ruby_dir, :bundler_config_path
|
|
37
|
+
|
|
38
|
+
def impl
|
|
39
|
+
# Skip if no Gemfile
|
|
40
|
+
return Skip unless CopyGemfile.gemfile_exists
|
|
41
|
+
|
|
42
|
+
# Skip cache if --no-cache is specified
|
|
43
|
+
return FromSource if Taski.args[:no_cache]
|
|
44
|
+
|
|
45
|
+
cache_exists? ? FromCache : FromSource
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Restore bundle from cache
|
|
49
|
+
class FromCache < Taski::Task
|
|
50
|
+
include BundleCacheHelpers
|
|
51
|
+
|
|
52
|
+
def run
|
|
53
|
+
work_dir = WorkDir.path
|
|
54
|
+
ruby_major_minor = InstallRuby.ruby_major_minor
|
|
55
|
+
|
|
56
|
+
@bundle_ruby_dir = File.join(work_dir, 'bundle', 'ruby', "#{ruby_major_minor}.0")
|
|
57
|
+
@bundler_config_path = File.join(work_dir, '.bundle', 'config')
|
|
58
|
+
|
|
59
|
+
kompo_cache = Taski.args.fetch(:kompo_cache, File.expand_path('~/.kompo/cache'))
|
|
60
|
+
ruby_version = InstallRuby.ruby_version
|
|
61
|
+
version_cache_dir = File.join(kompo_cache, ruby_version)
|
|
62
|
+
|
|
63
|
+
bundle_cache_name = compute_bundle_cache_name
|
|
64
|
+
raise "Gemfile.lock not found in #{work_dir}" unless bundle_cache_name
|
|
65
|
+
|
|
66
|
+
cache_dir = File.join(version_cache_dir, bundle_cache_name)
|
|
67
|
+
|
|
68
|
+
group('Restoring bundle from cache') do
|
|
69
|
+
# Clean up existing files in case work_dir is reused
|
|
70
|
+
FileUtils.rm_rf(File.join(work_dir, 'bundle')) if Dir.exist?(File.join(work_dir, 'bundle'))
|
|
71
|
+
FileUtils.rm_rf(File.join(work_dir, '.bundle')) if Dir.exist?(File.join(work_dir, '.bundle'))
|
|
72
|
+
|
|
73
|
+
# Copy from cache
|
|
74
|
+
FileUtils.cp_r(File.join(cache_dir, 'bundle'), File.join(work_dir, 'bundle'))
|
|
75
|
+
FileUtils.cp_r(File.join(cache_dir, '.bundle'), File.join(work_dir, '.bundle'))
|
|
76
|
+
|
|
77
|
+
puts "Restored from: #{cache_dir}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
puts 'Bundle restored from cache'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clean
|
|
84
|
+
work_dir = WorkDir.path
|
|
85
|
+
return unless work_dir && Dir.exist?(work_dir)
|
|
86
|
+
|
|
87
|
+
[@bundle_ruby_dir, @bundler_config_path].each do |path|
|
|
88
|
+
next unless path
|
|
89
|
+
|
|
90
|
+
FileUtils.rm_rf(path) if File.exist?(path)
|
|
91
|
+
end
|
|
92
|
+
puts 'Cleaned up bundle installation'
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Run bundle install and save to cache
|
|
97
|
+
class FromSource < Taski::Task
|
|
98
|
+
include BundleCacheHelpers
|
|
99
|
+
|
|
100
|
+
def run
|
|
101
|
+
work_dir = WorkDir.path
|
|
102
|
+
bundler = InstallRuby.bundler_path
|
|
103
|
+
ruby_major_minor = InstallRuby.ruby_major_minor
|
|
104
|
+
|
|
105
|
+
@bundle_ruby_dir = File.join(work_dir, 'bundle', 'ruby', "#{ruby_major_minor}.0")
|
|
106
|
+
@bundler_config_path = File.join(work_dir, '.bundle', 'config')
|
|
107
|
+
|
|
108
|
+
puts 'Running bundle install --path bundle...'
|
|
109
|
+
gemfile_path = File.join(work_dir, 'Gemfile')
|
|
110
|
+
|
|
111
|
+
# Clear Bundler environment and specify Gemfile path explicitly
|
|
112
|
+
Bundler.with_unbundled_env do
|
|
113
|
+
ruby = InstallRuby.ruby_path
|
|
114
|
+
env = { 'BUNDLE_GEMFILE' => gemfile_path }
|
|
115
|
+
|
|
116
|
+
# Suppress clang 18+ warning that causes mkmf try_cppflags to fail
|
|
117
|
+
# This flag is clang-specific and not recognized by GCC
|
|
118
|
+
if clang_compiler?
|
|
119
|
+
env['CFLAGS'] = '-Wno-default-const-init-field-unsafe'
|
|
120
|
+
env['CPPFLAGS'] = '-Wno-default-const-init-field-unsafe'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Set BUNDLE_PATH to "bundle" - standard Bundler reads .bundle/config
|
|
124
|
+
# and finds gems in {BUNDLE_PATH}/ruby/X.X.X/gems/
|
|
125
|
+
# Use ruby to execute bundler to avoid shebang issues
|
|
126
|
+
system({ 'BUNDLE_GEMFILE' => gemfile_path }, ruby, bundler, 'config', 'set', '--local', 'path',
|
|
127
|
+
'bundle') or raise 'Failed to set bundle path'
|
|
128
|
+
system(env, ruby, bundler, 'install') or raise 'Failed to run bundle install'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
puts 'Bundle installed successfully'
|
|
132
|
+
|
|
133
|
+
# Save to cache
|
|
134
|
+
save_to_cache(work_dir)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def clean
|
|
138
|
+
work_dir = WorkDir.path
|
|
139
|
+
return unless work_dir && Dir.exist?(work_dir)
|
|
140
|
+
|
|
141
|
+
[@bundle_ruby_dir, @bundler_config_path].each do |path|
|
|
142
|
+
next unless path
|
|
143
|
+
|
|
144
|
+
FileUtils.rm_rf(path) if File.exist?(path)
|
|
145
|
+
end
|
|
146
|
+
puts 'Cleaned up bundle installation'
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def clang_compiler?
|
|
152
|
+
output = `cc --version 2>&1`
|
|
153
|
+
output.include?('clang')
|
|
154
|
+
rescue Errno::ENOENT => e
|
|
155
|
+
warn "cc command not found: #{e.message}"
|
|
156
|
+
false
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
warn "Error checking compiler: #{e.message}"
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def save_to_cache(work_dir)
|
|
163
|
+
bundle_cache_name = compute_bundle_cache_name
|
|
164
|
+
return unless bundle_cache_name
|
|
165
|
+
|
|
166
|
+
kompo_cache = Taski.args.fetch(:kompo_cache, File.expand_path('~/.kompo/cache'))
|
|
167
|
+
ruby_version = InstallRuby.ruby_version
|
|
168
|
+
version_cache_dir = File.join(kompo_cache, ruby_version)
|
|
169
|
+
cache_dir = File.join(version_cache_dir, bundle_cache_name)
|
|
170
|
+
|
|
171
|
+
group('Saving bundle to cache') do
|
|
172
|
+
# Remove old cache if exists
|
|
173
|
+
FileUtils.rm_rf(cache_dir) if Dir.exist?(cache_dir)
|
|
174
|
+
FileUtils.mkdir_p(cache_dir)
|
|
175
|
+
|
|
176
|
+
# Copy to cache
|
|
177
|
+
FileUtils.cp_r(File.join(work_dir, 'bundle'), File.join(cache_dir, 'bundle'))
|
|
178
|
+
FileUtils.cp_r(File.join(work_dir, '.bundle'), File.join(cache_dir, '.bundle'))
|
|
179
|
+
|
|
180
|
+
# Save metadata
|
|
181
|
+
metadata = {
|
|
182
|
+
'ruby_version' => ruby_version,
|
|
183
|
+
'gemfile_lock_hash' => compute_gemfile_lock_hash,
|
|
184
|
+
'created_at' => Time.now.iso8601
|
|
185
|
+
}
|
|
186
|
+
File.write(File.join(cache_dir, 'metadata.json'), JSON.pretty_generate(metadata))
|
|
187
|
+
|
|
188
|
+
puts "Saved to: #{cache_dir}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Skip bundle install when no Gemfile
|
|
194
|
+
class Skip < Taski::Task
|
|
195
|
+
def run
|
|
196
|
+
puts 'No Gemfile, skipping bundle install'
|
|
197
|
+
@bundle_ruby_dir = nil
|
|
198
|
+
@bundler_config_path = nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def clean
|
|
202
|
+
# Nothing to clean
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def cache_exists?
|
|
209
|
+
bundle_cache_name = compute_bundle_cache_name
|
|
210
|
+
return false unless bundle_cache_name
|
|
211
|
+
|
|
212
|
+
kompo_cache = Taski.args.fetch(:kompo_cache, File.expand_path('~/.kompo/cache'))
|
|
213
|
+
ruby_version = InstallRuby.ruby_version
|
|
214
|
+
version_cache_dir = File.join(kompo_cache, ruby_version)
|
|
215
|
+
cache_dir = File.join(version_cache_dir, bundle_cache_name)
|
|
216
|
+
|
|
217
|
+
cache_bundle_dir = File.join(cache_dir, 'bundle')
|
|
218
|
+
cache_bundle_config = File.join(cache_dir, '.bundle')
|
|
219
|
+
cache_metadata = File.join(cache_dir, 'metadata.json')
|
|
220
|
+
|
|
221
|
+
Dir.exist?(cache_bundle_dir) && Dir.exist?(cache_bundle_config) && File.exist?(cache_metadata)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Kompo
|
|
6
|
+
# Section to get the Cargo path.
|
|
7
|
+
# Switches implementation based on whether Cargo is already installed.
|
|
8
|
+
class CargoPath < Taski::Section
|
|
9
|
+
interfaces :path
|
|
10
|
+
|
|
11
|
+
def impl
|
|
12
|
+
cargo_installed? ? Installed : Install
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Use existing Cargo installation
|
|
16
|
+
class Installed < Taski::Task
|
|
17
|
+
def run
|
|
18
|
+
# First check PATH, then fallback to default rustup location
|
|
19
|
+
cargo_in_path, = Open3.capture2('which', 'cargo', err: File::NULL)
|
|
20
|
+
cargo_in_path = cargo_in_path.chomp
|
|
21
|
+
@path = if cargo_in_path.empty?
|
|
22
|
+
File.expand_path('~/.cargo/bin/cargo')
|
|
23
|
+
else
|
|
24
|
+
cargo_in_path
|
|
25
|
+
end
|
|
26
|
+
puts "Cargo path: #{@path}"
|
|
27
|
+
version_output, = Open3.capture2e(@path, '--version')
|
|
28
|
+
puts "Cargo version: #{version_output.chomp}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Install Cargo via rustup and return the path
|
|
33
|
+
class Install < Taski::Task
|
|
34
|
+
def run
|
|
35
|
+
puts 'Cargo not found. Installing via rustup...'
|
|
36
|
+
system("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
|
|
37
|
+
|
|
38
|
+
@path = File.expand_path('~/.cargo/bin/cargo')
|
|
39
|
+
raise 'Failed to install Cargo' unless File.executable?(@path)
|
|
40
|
+
|
|
41
|
+
puts "Cargo installed at: #{@path}"
|
|
42
|
+
version_output, = Open3.capture2e(@path, '--version')
|
|
43
|
+
puts "Cargo version: #{version_output.chomp}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def cargo_installed?
|
|
50
|
+
# Check if cargo is in PATH
|
|
51
|
+
cargo_in_path, = Open3.capture2('which', 'cargo', err: File::NULL)
|
|
52
|
+
return true unless cargo_in_path.chomp.empty?
|
|
53
|
+
|
|
54
|
+
# Check default rustup installation location
|
|
55
|
+
home_cargo = File.expand_path('~/.cargo/bin/cargo')
|
|
56
|
+
File.executable?(home_cargo)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Kompo
|
|
6
|
+
# Get Ruby standard library paths from installed Ruby
|
|
7
|
+
class CheckStdlibs < Taski::Task
|
|
8
|
+
exports :paths
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
# Check if stdlib should be excluded
|
|
12
|
+
no_stdlib = Taski.args.fetch(:no_stdlib, false)
|
|
13
|
+
if no_stdlib
|
|
14
|
+
@paths = []
|
|
15
|
+
puts 'Skipping standard library (--no-stdlib)'
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ruby = InstallRuby.ruby_path
|
|
20
|
+
ruby_install_dir = InstallRuby.ruby_install_dir
|
|
21
|
+
original_ruby_install_dir = InstallRuby.original_ruby_install_dir
|
|
22
|
+
ruby_major_minor = InstallRuby.ruby_major_minor
|
|
23
|
+
|
|
24
|
+
# Include the Ruby standard library root directory
|
|
25
|
+
# This includes bundler and other default gems that are not in $:
|
|
26
|
+
# Ruby uses "X.Y.0" format for lib/ruby paths (e.g., "3.4.0" not "3.4")
|
|
27
|
+
stdlib_root = File.join(ruby_install_dir, 'lib', 'ruby', "#{ruby_major_minor}.0")
|
|
28
|
+
# RubyGems needs gemspec files in specifications/ directory
|
|
29
|
+
gems_specs_root = File.join(ruby_install_dir, 'lib', 'ruby', 'gems', "#{ruby_major_minor}.0", 'specifications')
|
|
30
|
+
|
|
31
|
+
if Dir.exist?(stdlib_root)
|
|
32
|
+
@paths = [stdlib_root, gems_specs_root].select { |p| Dir.exist?(p) }
|
|
33
|
+
puts "Including Ruby standard library: #{stdlib_root}"
|
|
34
|
+
puts "Including gem specifications: #{gems_specs_root}" if Dir.exist?(gems_specs_root)
|
|
35
|
+
else
|
|
36
|
+
# Fallback to $: paths if stdlib root doesn't exist
|
|
37
|
+
output, status = Open3.capture2(ruby, '-e', 'puts $:', err: File::NULL)
|
|
38
|
+
unless status.success?
|
|
39
|
+
raise "Failed to get Ruby standard library paths: exit code #{status.exitstatus}, output: #{output}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
raw_paths = output.split("\n").reject(&:empty?)
|
|
43
|
+
|
|
44
|
+
@paths = raw_paths.map do |path|
|
|
45
|
+
next nil unless path.start_with?('/')
|
|
46
|
+
|
|
47
|
+
if original_ruby_install_dir != ruby_install_dir && path.start_with?(original_ruby_install_dir)
|
|
48
|
+
path.sub(original_ruby_install_dir, ruby_install_dir)
|
|
49
|
+
else
|
|
50
|
+
path
|
|
51
|
+
end
|
|
52
|
+
end.compact
|
|
53
|
+
|
|
54
|
+
puts "Found #{@paths.size} standard library paths (fallback)"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Kompo
|
|
6
|
+
# Collect all dependencies for packing.
|
|
7
|
+
# This task is used by Packing to get the values it needs.
|
|
8
|
+
class CollectDependencies < Taski::Task
|
|
9
|
+
exports :work_dir, :deps, :ext_paths, :enc_files, :output_path
|
|
10
|
+
|
|
11
|
+
Dependencies = Struct.new(
|
|
12
|
+
:ruby_install_dir, :ruby_version, :ruby_major_minor,
|
|
13
|
+
:ruby_build_path, :ruby_lib, :kompo_lib,
|
|
14
|
+
:main_c, :fs_c, :exts_dir, :deps_lib_paths,
|
|
15
|
+
keyword_init: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
@work_dir = WorkDir.path
|
|
20
|
+
project_dir = Taski.args.fetch(:project_dir, Taski.env.working_directory) || Taski.env.working_directory
|
|
21
|
+
output_arg = Taski.args.fetch(:output_dir, Taski.env.working_directory) || Taski.env.working_directory
|
|
22
|
+
|
|
23
|
+
@deps = group('Collecting dependencies') do
|
|
24
|
+
collect_dependencies
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# If output_arg is an existing directory, create binary inside it
|
|
28
|
+
# Otherwise, treat it as the output file path
|
|
29
|
+
if File.directory?(output_arg)
|
|
30
|
+
@output_path = File.join(output_arg, File.basename(project_dir))
|
|
31
|
+
else
|
|
32
|
+
@output_path = output_arg
|
|
33
|
+
# Ensure parent directory exists
|
|
34
|
+
FileUtils.mkdir_p(File.dirname(@output_path))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@ext_paths = get_unique_ext_paths(@work_dir, @deps.ruby_build_path, @deps.ruby_version, @deps.exts_dir)
|
|
38
|
+
@enc_files = [
|
|
39
|
+
File.join(@deps.ruby_build_path, "ruby-#{@deps.ruby_version}", 'enc/encinit.o'),
|
|
40
|
+
File.join(@deps.ruby_build_path, "ruby-#{@deps.ruby_version}", 'enc/libenc.a'),
|
|
41
|
+
File.join(@deps.ruby_build_path, "ruby-#{@deps.ruby_version}", 'enc/libtrans.a')
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
puts 'All dependencies collected for packing'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def collect_dependencies
|
|
50
|
+
# Install dependencies based on platform (Homebrew on macOS, pkg-config check on Linux)
|
|
51
|
+
InstallDeps.run
|
|
52
|
+
deps_lib_paths = InstallDeps.lib_paths
|
|
53
|
+
|
|
54
|
+
ruby_install_dir = InstallRuby.ruby_install_dir
|
|
55
|
+
ruby_version = InstallRuby.ruby_version
|
|
56
|
+
ruby_major_minor = InstallRuby.ruby_major_minor
|
|
57
|
+
ruby_build_path = InstallRuby.ruby_build_path
|
|
58
|
+
ruby_lib = File.join(ruby_install_dir, 'lib')
|
|
59
|
+
|
|
60
|
+
kompo_lib = KompoVfsPath.path
|
|
61
|
+
main_c = MakeMainC.path
|
|
62
|
+
fs_c = MakeFsC.path
|
|
63
|
+
exts_dir = BuildNativeGem.exts_dir
|
|
64
|
+
|
|
65
|
+
puts 'Dependencies collected'
|
|
66
|
+
|
|
67
|
+
Dependencies.new(
|
|
68
|
+
ruby_install_dir: ruby_install_dir,
|
|
69
|
+
ruby_version: ruby_version,
|
|
70
|
+
ruby_major_minor: ruby_major_minor,
|
|
71
|
+
ruby_build_path: ruby_build_path,
|
|
72
|
+
ruby_lib: ruby_lib,
|
|
73
|
+
kompo_lib: kompo_lib,
|
|
74
|
+
main_c: main_c,
|
|
75
|
+
fs_c: fs_c,
|
|
76
|
+
exts_dir: exts_dir,
|
|
77
|
+
deps_lib_paths: deps_lib_paths
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def get_unique_ext_paths(_work_dir, ruby_build_path, ruby_version, exts_dir)
|
|
82
|
+
# Ruby standard extension .o files
|
|
83
|
+
paths = Dir.glob(File.join(ruby_build_path, "ruby-#{ruby_version}", 'ext', '**', '*.o'))
|
|
84
|
+
|
|
85
|
+
# Extract extension path (everything after /ext/) for deduplication
|
|
86
|
+
# e.g., ".../ext/cgi/escape/escape.o" -> "cgi/escape/escape.o"
|
|
87
|
+
ruby_ext_keys = paths.map { |p| p.split('/ext/').last }
|
|
88
|
+
|
|
89
|
+
# Gem extension .o files (excluding duplicates with Ruby std)
|
|
90
|
+
if exts_dir && Dir.exist?(exts_dir)
|
|
91
|
+
gem_ext_paths = Dir.glob("#{exts_dir}/**/*.o")
|
|
92
|
+
.to_h { |p| [p.split('/ext/').last, p] }
|
|
93
|
+
.except(*ruby_ext_keys)
|
|
94
|
+
.values
|
|
95
|
+
paths += gem_ext_paths
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
paths
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Kompo
|
|
6
|
+
# Copy Gemfile and Gemfile.lock to working directory if they exist
|
|
7
|
+
class CopyGemfile < Taski::Task
|
|
8
|
+
exports :gemfile_exists
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
work_dir = WorkDir.path
|
|
12
|
+
project_dir = Taski.args.fetch(:project_dir, Taski.env.working_directory) || Taski.env.working_directory
|
|
13
|
+
|
|
14
|
+
gemfile_path = File.join(project_dir, 'Gemfile')
|
|
15
|
+
gemfile_lock_path = File.join(project_dir, 'Gemfile.lock')
|
|
16
|
+
|
|
17
|
+
@gemfile_exists = File.exist?(gemfile_path)
|
|
18
|
+
|
|
19
|
+
if @gemfile_exists
|
|
20
|
+
FileUtils.cp(gemfile_path, work_dir)
|
|
21
|
+
puts 'Copied: Gemfile'
|
|
22
|
+
|
|
23
|
+
if File.exist?(gemfile_lock_path)
|
|
24
|
+
FileUtils.cp(gemfile_lock_path, work_dir)
|
|
25
|
+
puts 'Copied: Gemfile.lock'
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
puts 'No Gemfile found, skipping'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clean
|
|
33
|
+
return unless @gemfile_exists
|
|
34
|
+
|
|
35
|
+
work_dir = WorkDir.path
|
|
36
|
+
return unless work_dir && Dir.exist?(work_dir)
|
|
37
|
+
|
|
38
|
+
gemfile = File.join(work_dir, 'Gemfile')
|
|
39
|
+
gemfile_lock = File.join(work_dir, 'Gemfile.lock')
|
|
40
|
+
|
|
41
|
+
FileUtils.rm_f(gemfile)
|
|
42
|
+
FileUtils.rm_f(gemfile_lock)
|
|
43
|
+
puts 'Cleaned up Gemfile'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|