kompo 0.2.0 → 0.3.1
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 +27 -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 +78 -0
- data/lib/kompo/tasks/copy_project_files.rb +112 -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,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Kompo
|
|
6
|
+
# Copy Gemfile, Gemfile.lock, and gemspec files 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
|
+
@copied_gemspecs = []
|
|
19
|
+
|
|
20
|
+
if @gemfile_exists
|
|
21
|
+
FileUtils.cp(gemfile_path, work_dir)
|
|
22
|
+
puts 'Copied: Gemfile'
|
|
23
|
+
|
|
24
|
+
if File.exist?(gemfile_lock_path)
|
|
25
|
+
FileUtils.cp(gemfile_lock_path, work_dir)
|
|
26
|
+
puts 'Copied: Gemfile.lock'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Copy gemspec files if Gemfile references gemspec
|
|
30
|
+
copy_gemspec_if_needed(gemfile_path, project_dir, work_dir)
|
|
31
|
+
else
|
|
32
|
+
puts 'No Gemfile found, skipping'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clean
|
|
37
|
+
return unless @gemfile_exists
|
|
38
|
+
|
|
39
|
+
work_dir = WorkDir.path
|
|
40
|
+
return unless work_dir && Dir.exist?(work_dir)
|
|
41
|
+
|
|
42
|
+
gemfile = File.join(work_dir, 'Gemfile')
|
|
43
|
+
gemfile_lock = File.join(work_dir, 'Gemfile.lock')
|
|
44
|
+
|
|
45
|
+
FileUtils.rm_f(gemfile)
|
|
46
|
+
FileUtils.rm_f(gemfile_lock)
|
|
47
|
+
|
|
48
|
+
# Clean up copied gemspec files
|
|
49
|
+
(@copied_gemspecs || []).each do |gemspec|
|
|
50
|
+
FileUtils.rm_f(gemspec)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
puts 'Cleaned up Gemfile'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def copy_gemspec_if_needed(gemfile_path, project_dir, work_dir)
|
|
59
|
+
gemfile_content = File.read(gemfile_path)
|
|
60
|
+
|
|
61
|
+
# Check if Gemfile contains a gemspec directive
|
|
62
|
+
return unless gemfile_content.match?(/^\s*gemspec\b/)
|
|
63
|
+
|
|
64
|
+
# Copy all .gemspec files from project directory
|
|
65
|
+
gemspec_files = Dir.glob(File.join(project_dir, '*.gemspec'))
|
|
66
|
+
gemspec_files.each do |gemspec_path|
|
|
67
|
+
dest_path = File.join(work_dir, File.basename(gemspec_path))
|
|
68
|
+
FileUtils.cp(gemspec_path, dest_path)
|
|
69
|
+
@copied_gemspecs << dest_path
|
|
70
|
+
puts "Copied: #{File.basename(gemspec_path)}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if gemspec_files.empty?
|
|
74
|
+
warn 'Warning: Gemfile contains gemspec directive but no .gemspec files found'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
# Special handling for "." - copy directory contents directly to work_dir
|
|
57
|
+
if file == '.' || real_src == real_project_dir
|
|
58
|
+
copy_directory_contents(src, work_dir)
|
|
59
|
+
@additional_paths << work_dir
|
|
60
|
+
else
|
|
61
|
+
FileUtils.mkdir_p(dest)
|
|
62
|
+
FileUtils.cp_r(src, File.dirname(dest))
|
|
63
|
+
@additional_paths << dest
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
67
|
+
FileUtils.cp(src, dest)
|
|
68
|
+
@additional_paths << dest
|
|
69
|
+
end
|
|
70
|
+
puts "Copied: #{file}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def clean
|
|
75
|
+
work_dir = WorkDir.path
|
|
76
|
+
return unless work_dir && Dir.exist?(work_dir)
|
|
77
|
+
|
|
78
|
+
real_work_dir = File.realpath(work_dir)
|
|
79
|
+
files = Taski.args.fetch(:files, [])
|
|
80
|
+
|
|
81
|
+
# Clean up copied files (with path validation)
|
|
82
|
+
if @entrypoint_path
|
|
83
|
+
expanded_path = File.expand_path(@entrypoint_path)
|
|
84
|
+
FileUtils.rm_f(@entrypoint_path) if expanded_path.start_with?(real_work_dir + File::SEPARATOR)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
files.each do |file|
|
|
88
|
+
path = File.join(work_dir, file)
|
|
89
|
+
expanded_path = File.expand_path(path)
|
|
90
|
+
# Only delete if path is inside work_dir
|
|
91
|
+
FileUtils.rm_rf(path) if expanded_path.start_with?(real_work_dir + File::SEPARATOR) && File.exist?(path)
|
|
92
|
+
end
|
|
93
|
+
puts 'Cleaned up project files'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def copy_directory_contents(src_dir, dest_dir)
|
|
99
|
+
# Copy all files and directories from src_dir directly into dest_dir
|
|
100
|
+
Dir.each_child(src_dir) do |child|
|
|
101
|
+
src_path = File.join(src_dir, child)
|
|
102
|
+
dest_path = File.join(dest_dir, child)
|
|
103
|
+
|
|
104
|
+
if File.directory?(src_path)
|
|
105
|
+
FileUtils.cp_r(src_path, dest_path)
|
|
106
|
+
else
|
|
107
|
+
FileUtils.cp(src_path, dest_path)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
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
|