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,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Kompo
6
+ # Copy project files (entrypoint and additional files) to working directory
7
+ class CopyProjectFiles < Taski::Task
8
+ exports :entrypoint_path, :additional_paths
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
+ entrypoint = Taski.args.fetch(:entrypoint, 'main.rb')
14
+ files = Taski.args.fetch(:files, [])
15
+
16
+ # Copy entrypoint (preserve relative path structure)
17
+ src_entrypoint = File.expand_path(File.join(project_dir, entrypoint))
18
+ raise "Entrypoint not found: #{src_entrypoint}" unless File.exist?(src_entrypoint)
19
+
20
+ # Validate source is inside project_dir
21
+ real_project_dir = File.realpath(project_dir)
22
+ real_src = File.realpath(src_entrypoint)
23
+ unless real_src.start_with?(real_project_dir + File::SEPARATOR) || real_src == real_project_dir
24
+ raise "Entrypoint path escapes project directory: #{entrypoint}"
25
+ end
26
+
27
+ @entrypoint_path = File.join(work_dir, entrypoint)
28
+ FileUtils.mkdir_p(File.dirname(@entrypoint_path))
29
+ FileUtils.cp(src_entrypoint, @entrypoint_path)
30
+ puts "Copied entrypoint: #{entrypoint}"
31
+
32
+ # Copy additional files/directories
33
+ @additional_paths = []
34
+ files.each do |file|
35
+ src = File.expand_path(File.join(project_dir, file))
36
+ next unless File.exist?(src)
37
+
38
+ # Validate source is inside project_dir
39
+ real_src = File.realpath(src)
40
+ unless real_src.start_with?(real_project_dir + File::SEPARATOR) || real_src == real_project_dir
41
+ warn "Skipping path that escapes project directory: #{file}"
42
+ next
43
+ end
44
+
45
+ dest = File.join(work_dir, file)
46
+
47
+ # Validate destination is inside work_dir
48
+ real_work_dir = File.realpath(work_dir)
49
+ expanded_dest = File.expand_path(dest)
50
+ unless expanded_dest.start_with?(real_work_dir + File::SEPARATOR) || expanded_dest == real_work_dir
51
+ warn "Skipping path that would escape work directory: #{file}"
52
+ next
53
+ end
54
+
55
+ if File.directory?(src)
56
+ FileUtils.mkdir_p(dest)
57
+ FileUtils.cp_r(src, File.dirname(dest))
58
+ else
59
+ FileUtils.mkdir_p(File.dirname(dest))
60
+ FileUtils.cp(src, dest)
61
+ end
62
+ @additional_paths << dest
63
+ puts "Copied: #{file}"
64
+ end
65
+ end
66
+
67
+ def clean
68
+ work_dir = WorkDir.path
69
+ return unless work_dir && Dir.exist?(work_dir)
70
+
71
+ real_work_dir = File.realpath(work_dir)
72
+ files = Taski.args.fetch(:files, [])
73
+
74
+ # Clean up copied files (with path validation)
75
+ if @entrypoint_path
76
+ expanded_path = File.expand_path(@entrypoint_path)
77
+ FileUtils.rm_f(@entrypoint_path) if expanded_path.start_with?(real_work_dir + File::SEPARATOR)
78
+ end
79
+
80
+ files.each do |file|
81
+ path = File.join(work_dir, file)
82
+ expanded_path = File.expand_path(path)
83
+ # Only delete if path is inside work_dir
84
+ FileUtils.rm_rf(path) if expanded_path.start_with?(real_work_dir + File::SEPARATOR) && File.exist?(path)
85
+ end
86
+ puts 'Cleaned up project files'
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Kompo
6
+ # Find native gem extensions that need to be built
7
+ # Exports:
8
+ # - extensions: Array of hashes with extension info (dir_name, gem_ext_name, is_rust)
9
+ class FindNativeExtensions < Taski::Task
10
+ exports :extensions
11
+
12
+ def run
13
+ @extensions = []
14
+
15
+ # Skip if no Gemfile
16
+ unless CopyGemfile.gemfile_exists
17
+ puts 'No Gemfile, no native extensions to find'
18
+ return
19
+ end
20
+
21
+ ruby_version = InstallRuby.ruby_version
22
+ ruby_build_path = InstallRuby.ruby_build_path
23
+ ruby_install_dir = InstallRuby.ruby_install_dir
24
+ bundle_ruby_dir = BundleInstall.bundle_ruby_dir
25
+
26
+ # Get Init functions already defined in libruby-static
27
+ builtin_init_funcs = get_builtin_init_functions(ruby_install_dir)
28
+
29
+ # Find all native extensions in installed gems
30
+ extconf_files = Dir.glob(File.join(bundle_ruby_dir, 'gems/**/extconf.rb'))
31
+
32
+ extconf_files.each do |extconf_path|
33
+ dir_name = File.dirname(extconf_path)
34
+ gem_ext_name = File.basename(dir_name)
35
+
36
+ # Skip if Init function is already defined in libruby-static
37
+ # This catches gems like prism that are compiled into Ruby core
38
+ init_func = "Init_#{gem_ext_name}"
39
+ if builtin_init_funcs.include?(init_func)
40
+ puts "skip: #{gem_ext_name} is already built into Ruby (#{init_func} found in libruby-static)"
41
+ next
42
+ end
43
+
44
+ # Skip if this extension is part of Ruby standard library
45
+ ruby_std_lib = dir_name.split('/').drop_while { |p| p != 'ext' }.join('/')
46
+ ruby_ext_objects = Dir.glob(File.join(ruby_build_path, "ruby-#{ruby_version}", 'ext', '**', '*.o'))
47
+ if ruby_ext_objects.any? { |o| o.include?(ruby_std_lib) }
48
+ puts "skip: #{gem_ext_name} is included in Ruby standard library"
49
+ next
50
+ end
51
+
52
+ cargo_toml = File.join(dir_name, 'Cargo.toml')
53
+ is_rust = File.exist?(cargo_toml)
54
+
55
+ @extensions << {
56
+ dir_name: dir_name,
57
+ gem_ext_name: gem_ext_name,
58
+ is_rust: is_rust,
59
+ cargo_toml: is_rust ? cargo_toml : nil
60
+ }
61
+ end
62
+
63
+ puts "Found #{@extensions.size} native extensions to build"
64
+ end
65
+
66
+ private
67
+
68
+ # Extract Init_ function names from libruby-static using nm
69
+ def get_builtin_init_functions(ruby_install_dir)
70
+ lib_dir = File.join(ruby_install_dir, 'lib')
71
+ static_lib = Dir.glob(File.join(lib_dir, 'libruby*-static.a')).first
72
+ return Set.new unless static_lib
73
+
74
+ # Use nm to extract defined Init_ symbols (T = text/code section)
75
+ # Use Open3.capture2 with array form to avoid shell injection
76
+ output, status = Open3.capture2('nm', static_lib, err: File::NULL)
77
+ return Set.new unless status.success?
78
+
79
+ # Filter lines matching " T _?Init_" pattern and extract the symbol name
80
+ # macOS prefixes symbols with underscore, Linux does not
81
+ symbols = output.lines.select { |line| line.match?(/ T _?Init_/) }
82
+ symbols.map do |line|
83
+ # Third whitespace-separated field is the symbol name
84
+ symbol = line.split[2]
85
+ symbol&.delete_prefix('_')
86
+ end.compact.to_set
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Kompo
6
+ # Section to get the Homebrew path.
7
+ # Switches implementation based on whether Homebrew is already installed.
8
+ class HomebrewPath < Taski::Section
9
+ interfaces :path
10
+
11
+ # Common Homebrew installation paths
12
+ COMMON_BREW_PATHS = ['/opt/homebrew/bin/brew', '/usr/local/bin/brew'].freeze
13
+
14
+ def impl
15
+ homebrew_installed? ? Installed : Install
16
+ end
17
+
18
+ # Use existing Homebrew installation
19
+ class Installed < Taski::Task
20
+ def run
21
+ # First check PATH
22
+ brew_in_path, = Open3.capture2('which', 'brew', err: File::NULL)
23
+ brew_in_path = brew_in_path.chomp
24
+ @path = if brew_in_path.empty?
25
+ # Fallback to common installation paths
26
+ HomebrewPath::COMMON_BREW_PATHS.find { |p| File.executable?(p) }
27
+ else
28
+ brew_in_path
29
+ end
30
+ puts "Homebrew path: #{@path}"
31
+ end
32
+ end
33
+
34
+ # Install Homebrew
35
+ class Install < Taski::Task
36
+ MARKER_FILE = File.expand_path('~/.kompo_installed_homebrew')
37
+
38
+ def run
39
+ puts 'Homebrew not found. Installing...'
40
+ system('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"')
41
+
42
+ brew_in_path, = Open3.capture2('which', 'brew', err: File::NULL)
43
+ @path = brew_in_path.chomp
44
+ if @path.empty?
45
+ # Check common installation paths
46
+ HomebrewPath::COMMON_BREW_PATHS.each do |p|
47
+ if File.executable?(p)
48
+ @path = p
49
+ break
50
+ end
51
+ end
52
+ end
53
+
54
+ raise 'Failed to install Homebrew' if @path.nil? || @path.empty?
55
+
56
+ # Mark that kompo installed Homebrew
57
+ File.write(MARKER_FILE, @path)
58
+ puts "Homebrew installed at: #{@path}"
59
+ end
60
+
61
+ def clean
62
+ # Only uninstall if kompo installed it
63
+ return unless File.exist?(MARKER_FILE)
64
+
65
+ puts 'Uninstalling Homebrew (installed by kompo)...'
66
+ system('NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"')
67
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
68
+ puts 'Homebrew uninstalled'
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def homebrew_installed?
75
+ # Check if brew is in PATH
76
+ brew_in_path, = Open3.capture2('which', 'brew', err: File::NULL)
77
+ return true unless brew_in_path.chomp.empty?
78
+
79
+ # Check common installation paths
80
+ COMMON_BREW_PATHS.any? { |p| File.executable?(p) }
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ module Kompo
5
+ # Section to handle platform-specific dependencies.
6
+ # Switches implementation based on the current platform.
7
+ # Exports lib_paths for linker flags (e.g., "-L/usr/local/lib")
8
+ class InstallDeps < Taski::Section
9
+ interfaces :lib_paths
10
+
11
+ def impl
12
+ macos? ? ForMacOS : ForLinux
13
+ end
14
+
15
+ # macOS implementation - installs dependencies via Homebrew
16
+ class ForMacOS < Taski::Task
17
+ def run
18
+ # HomebrewPath.path triggers Homebrew installation if not present
19
+ @lib_paths = [
20
+ InstallGmp.lib_path,
21
+ InstallOpenssl.lib_path,
22
+ InstallReadline.lib_path,
23
+ InstallLibyaml.lib_path,
24
+ InstallZlib.lib_path,
25
+ InstallLibffi.lib_path
26
+ ].compact.join(' ')
27
+
28
+ puts 'All Homebrew dependencies installed'
29
+ end
30
+
31
+ # GMP library installation Section
32
+ class InstallGmp < Taski::Section
33
+ interfaces :lib_path
34
+
35
+ def impl
36
+ brew = HomebrewPath.path
37
+ system("#{brew} list #{BREW_NAME} > /dev/null 2>&1") ? Installed : Install
38
+ end
39
+
40
+ BREW_NAME = 'gmp'
41
+ MARKER_FILE = File.expand_path('~/.kompo_installed_gmp')
42
+
43
+ class Installed < Taski::Task
44
+ def run
45
+ brew = HomebrewPath.path
46
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
47
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
48
+ puts "#{BREW_NAME} is already installed"
49
+ end
50
+ end
51
+
52
+ class Install < Taski::Task
53
+ def run
54
+ brew = HomebrewPath.path
55
+ puts "Installing #{BREW_NAME}..."
56
+ system("#{brew} install #{BREW_NAME}") or raise "Failed to install #{BREW_NAME}"
57
+ File.write(MARKER_FILE, 'installed')
58
+
59
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
60
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
61
+ end
62
+
63
+ def clean
64
+ return unless File.exist?(MARKER_FILE)
65
+
66
+ brew = HomebrewPath.path
67
+ puts "Uninstalling #{BREW_NAME} (installed by kompo)..."
68
+ system("#{brew} uninstall #{BREW_NAME}")
69
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
70
+ end
71
+ end
72
+ end
73
+
74
+ # OpenSSL library installation Section
75
+ class InstallOpenssl < Taski::Section
76
+ interfaces :lib_path
77
+
78
+ def impl
79
+ brew = HomebrewPath.path
80
+ system("#{brew} list #{BREW_NAME} > /dev/null 2>&1") ? Installed : Install
81
+ end
82
+
83
+ BREW_NAME = 'openssl@3'
84
+ MARKER_FILE = File.expand_path('~/.kompo_installed_openssl')
85
+
86
+ class Installed < Taski::Task
87
+ def run
88
+ brew = HomebrewPath.path
89
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
90
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
91
+ puts "#{BREW_NAME} is already installed"
92
+ end
93
+ end
94
+
95
+ class Install < Taski::Task
96
+ def run
97
+ brew = HomebrewPath.path
98
+ puts "Installing #{BREW_NAME}..."
99
+ system("#{brew} install #{BREW_NAME}") or raise "Failed to install #{BREW_NAME}"
100
+ File.write(MARKER_FILE, 'installed')
101
+
102
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
103
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
104
+ end
105
+
106
+ def clean
107
+ return unless File.exist?(MARKER_FILE)
108
+
109
+ brew = HomebrewPath.path
110
+ puts "Uninstalling #{BREW_NAME} (installed by kompo)..."
111
+ system("#{brew} uninstall #{BREW_NAME}")
112
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Readline library installation Section
118
+ class InstallReadline < Taski::Section
119
+ interfaces :lib_path
120
+
121
+ def impl
122
+ brew = HomebrewPath.path
123
+ system("#{brew} list #{BREW_NAME} > /dev/null 2>&1") ? Installed : Install
124
+ end
125
+
126
+ BREW_NAME = 'readline'
127
+ MARKER_FILE = File.expand_path('~/.kompo_installed_readline')
128
+
129
+ class Installed < Taski::Task
130
+ def run
131
+ brew = HomebrewPath.path
132
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
133
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
134
+ puts "#{BREW_NAME} is already installed"
135
+ end
136
+ end
137
+
138
+ class Install < Taski::Task
139
+ def run
140
+ brew = HomebrewPath.path
141
+ puts "Installing #{BREW_NAME}..."
142
+ system("#{brew} install #{BREW_NAME}") or raise "Failed to install #{BREW_NAME}"
143
+ File.write(MARKER_FILE, 'installed')
144
+
145
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
146
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
147
+ end
148
+
149
+ def clean
150
+ return unless File.exist?(MARKER_FILE)
151
+
152
+ brew = HomebrewPath.path
153
+ puts "Uninstalling #{BREW_NAME} (installed by kompo)..."
154
+ system("#{brew} uninstall #{BREW_NAME}")
155
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
156
+ end
157
+ end
158
+ end
159
+
160
+ # libyaml library installation Section
161
+ class InstallLibyaml < Taski::Section
162
+ interfaces :lib_path
163
+
164
+ def impl
165
+ brew = HomebrewPath.path
166
+ system("#{brew} list #{BREW_NAME} > /dev/null 2>&1") ? Installed : Install
167
+ end
168
+
169
+ BREW_NAME = 'libyaml'
170
+ MARKER_FILE = File.expand_path('~/.kompo_installed_libyaml')
171
+
172
+ class Installed < Taski::Task
173
+ def run
174
+ brew = HomebrewPath.path
175
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
176
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
177
+ puts "#{BREW_NAME} is already installed"
178
+ end
179
+ end
180
+
181
+ class Install < Taski::Task
182
+ def run
183
+ brew = HomebrewPath.path
184
+ puts "Installing #{BREW_NAME}..."
185
+ system("#{brew} install #{BREW_NAME}") or raise "Failed to install #{BREW_NAME}"
186
+ File.write(MARKER_FILE, 'installed')
187
+
188
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
189
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
190
+ end
191
+
192
+ def clean
193
+ return unless File.exist?(MARKER_FILE)
194
+
195
+ brew = HomebrewPath.path
196
+ puts "Uninstalling #{BREW_NAME} (installed by kompo)..."
197
+ system("#{brew} uninstall #{BREW_NAME}")
198
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
199
+ end
200
+ end
201
+ end
202
+
203
+ # zlib library installation Section
204
+ class InstallZlib < Taski::Section
205
+ interfaces :lib_path
206
+
207
+ def impl
208
+ brew = HomebrewPath.path
209
+ system("#{brew} list #{BREW_NAME} > /dev/null 2>&1") ? Installed : Install
210
+ end
211
+
212
+ BREW_NAME = 'zlib'
213
+ MARKER_FILE = File.expand_path('~/.kompo_installed_zlib')
214
+
215
+ class Installed < Taski::Task
216
+ def run
217
+ brew = HomebrewPath.path
218
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
219
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
220
+ puts "#{BREW_NAME} is already installed"
221
+ end
222
+ end
223
+
224
+ class Install < Taski::Task
225
+ def run
226
+ brew = HomebrewPath.path
227
+ puts "Installing #{BREW_NAME}..."
228
+ system("#{brew} install #{BREW_NAME}") or raise "Failed to install #{BREW_NAME}"
229
+ File.write(MARKER_FILE, 'installed')
230
+
231
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
232
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
233
+ end
234
+
235
+ def clean
236
+ return unless File.exist?(MARKER_FILE)
237
+
238
+ brew = HomebrewPath.path
239
+ puts "Uninstalling #{BREW_NAME} (installed by kompo)..."
240
+ system("#{brew} uninstall #{BREW_NAME}")
241
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
242
+ end
243
+ end
244
+ end
245
+
246
+ # libffi library installation Section
247
+ class InstallLibffi < Taski::Section
248
+ interfaces :lib_path
249
+
250
+ def impl
251
+ brew = HomebrewPath.path
252
+ system("#{brew} list #{BREW_NAME} > /dev/null 2>&1") ? Installed : Install
253
+ end
254
+
255
+ BREW_NAME = 'libffi'
256
+ MARKER_FILE = File.expand_path('~/.kompo_installed_libffi')
257
+
258
+ class Installed < Taski::Task
259
+ def run
260
+ brew = HomebrewPath.path
261
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
262
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
263
+ puts "#{BREW_NAME} is already installed"
264
+ end
265
+ end
266
+
267
+ class Install < Taski::Task
268
+ def run
269
+ brew = HomebrewPath.path
270
+ puts "Installing #{BREW_NAME}..."
271
+ system("#{brew} install #{BREW_NAME}") or raise "Failed to install #{BREW_NAME}"
272
+ File.write(MARKER_FILE, 'installed')
273
+
274
+ prefix = `#{brew} --prefix #{BREW_NAME} 2>/dev/null`.chomp
275
+ @lib_path = "-L#{prefix}/lib" if $CHILD_STATUS.success? && !prefix.empty?
276
+ end
277
+
278
+ def clean
279
+ return unless File.exist?(MARKER_FILE)
280
+
281
+ brew = HomebrewPath.path
282
+ puts "Uninstalling #{BREW_NAME} (installed by kompo)..."
283
+ system("#{brew} uninstall #{BREW_NAME}")
284
+ File.delete(MARKER_FILE) if File.exist?(MARKER_FILE)
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ # Linux implementation - checks dependencies using pkg-config
291
+ class ForLinux < Taski::Task
292
+ def run
293
+ unless pkg_config_available?
294
+ puts '[WARNING] pkg-config not found. Skipping dependency check.'
295
+ puts 'Install pkg-config to enable automatic dependency verification.'
296
+ @lib_paths = ''
297
+ return
298
+ end
299
+
300
+ check_dependencies
301
+ @lib_paths = collect_lib_paths
302
+
303
+ puts 'All required development libraries are installed.'
304
+ end
305
+
306
+ REQUIRED_LIBS = {
307
+ 'openssl' => { pkg_config: 'openssl', apt: 'libssl-dev', yum: 'openssl-devel' },
308
+ 'readline' => { pkg_config: 'readline', apt: 'libreadline-dev', yum: 'readline-devel' },
309
+ 'zlib' => { pkg_config: 'zlib', apt: 'zlib1g-dev', yum: 'zlib-devel' },
310
+ 'libyaml' => { pkg_config: 'yaml-0.1', apt: 'libyaml-dev', yum: 'libyaml-devel' },
311
+ 'libffi' => { pkg_config: 'libffi', apt: 'libffi-dev', yum: 'libffi-devel' }
312
+ }.freeze
313
+
314
+ private
315
+
316
+ def pkg_config_available?
317
+ system('which pkg-config > /dev/null 2>&1')
318
+ end
319
+
320
+ def check_dependencies
321
+ missing = REQUIRED_LIBS.reject do |_, info|
322
+ system("pkg-config --exists #{info[:pkg_config]} 2>/dev/null")
323
+ end
324
+
325
+ raise build_error_message(missing) unless missing.empty?
326
+ end
327
+
328
+ def collect_lib_paths
329
+ pkg_names = REQUIRED_LIBS.values.map { |info| info[:pkg_config] }
330
+ paths = pkg_names.flat_map do |pkg|
331
+ `pkg-config --libs-only-L #{pkg} 2>/dev/null`.chomp.split
332
+ end
333
+ paths.uniq.join(' ')
334
+ end
335
+
336
+ def build_error_message(missing)
337
+ lib_names = missing.keys.join(', ')
338
+ apt_packages = missing.values.map { |info| info[:apt] }.join(' ')
339
+ yum_packages = missing.values.map { |info| info[:yum] }.join(' ')
340
+
341
+ <<~MSG
342
+ Missing required development libraries: #{lib_names}
343
+
344
+ Please install them using your package manager:
345
+
346
+ Ubuntu/Debian:
347
+ sudo apt-get update
348
+ sudo apt-get install -y #{apt_packages}
349
+
350
+ RHEL/CentOS/Fedora:
351
+ sudo yum install -y #{yum_packages}
352
+ # or: sudo dnf install -y #{yum_packages}
353
+
354
+ After installing, run kompo again.
355
+ MSG
356
+ end
357
+ end
358
+
359
+ private
360
+
361
+ def macos?
362
+ RUBY_PLATFORM.include?('darwin')
363
+ end
364
+ end
365
+ end