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.
@@ -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